1 Primer

This is an example on how to use clustering techniques to analyze insurance data, not a blue print or best practice that can be followed for any other analysis. Any data analysis, in particular in unsupervised learning, relies heavily on domain knowledge and the application at hand. Even for this data set several other reasonable approaches could have been taken given a different set of assumptions and goals.

2 The dataset

This is an CAS datasets. From the official description:

“The univariate dataset was collected by an unknown French private insurer and comprise 1,274 marine losses between the January 2003 and June 2006. The status of the claim (settled or opened) is determined at the end of June 2006.”

THUMBS UP: Who of you has experience in marine insurance business?

# install.packages("CASdatasets", repos = "http://cas.uqam.ca/pub/", type="source")
library(CASdatasets)
Registered S3 methods overwritten by 'htmltools':
  method               from         
  print.html           tools:rstudio
  print.shiny.tag      tools:rstudio
  print.shiny.tag.list tools:rstudio
seed_var <- 03052021
data(fremarine)

Data preview:

library(data.table)
cat_vars <- c("ShipBrand","ShipHull","Departement")
num_vars <- c("ShipPower","ShipEngNb","ShipEngYear","ShipBuildYear","ShipLength","ShipTonnage","InsuredValue","ClaimPaid")
dat4clustering <- as.data.table(fremarine)
dat4clustering <- dat4clustering[ClaimStatus=="settled",mget(c(num_vars,cat_vars))]
library(DT)
datatable(dat4clustering, filter = 'top', options = list(pageLength = 5, autoWidth = TRUE))

2.1 Story

We put us into the position of a fictional claims analyst that wants to understand the losses better, having the following questions / whishes in mind:

  • Being new in the position, I do not have a good overview of recent losses. Previous analyses where focused on single cases rather then the overall portfolio.

  • With a segmentation of our recent losses I could the monitor the loss development over time.

  • Unfortunately, we are not enough people to review each claim in depth. New claims that do not fit very well to the existing segmentation, e.g., are quite far away from all centroids, could be marked as outliers and trigger a detailed assessment.

  • Claims handlers would like to gain efficiency by specialising on certain portfolio segments (large ships vs small etc.)

  • My underwriting colleagues would like to understand our losses better but many of them do not have a quantitative background. With a few representative examples (e.g., cluster centroids), I could give them a simplified but complete overview over our recent losses.

2.2 Data set dictionary

The data set is of mixed data type:

sapply(fremarine,FUN=class)
    OccurDate     ReporDate     ShipCateg     ShipBrand     ShipPower     ShipEngNb   ShipEngYear ShipBuildYear      ShipHull    ShipLength 
       "Date"        "Date"      "factor"      "factor"     "integer"     "integer"     "integer"     "integer"      "factor"     "numeric" 
  ShipTonnage  InsuredValue     ClaimPaid   ClaimCharge ClaimRecourse    ClaimCateg   ClaimStatus    Deductible   HeadQuarter   Departement 
    "numeric"     "numeric"     "numeric"     "numeric"     "numeric"      "factor"      "factor"     "numeric"      "factor"      "factor" 

A (somewhat brief) explanation can be found in the manual and is shown below:

Date variables:

  • OccurDate: The day of claim occurence
  • ReporDate: The day of claim reporting.ShipCategThe category of the insured ship

Factor variables:

  • ShipBrand: The brand of the insured ship
  • ShipHull: The hull of the insured ship
  • Departement: The French region in which the ship is headquartered

Numeric variables:

  • ShipPower:The power of the insured ship
  • ShipEngNb: The engine number of the insured ship
  • ShipEngYear: The engine year of the insured ship
  • ShipBuildYear: The building year of the insured ship
  • ShipLength: The length of the insured ship
  • ShipTonnage: The tonnage of the insured ship
  • InsuredValue: The insured value of the insured ship
  • ClaimPaid: The paid amount (EUR) of the claim
  • ClaimStatus: The status of the claim. Unsettled

2.3 Assumptions

For simplicity, we assume the paid claims includes the final amount paid to the policyholder after deductible, recourse etc. We will also restrict our analysis to settled claims only for which we have complete and final information.

Additionally, we decide not to use the date since:

  • Our (fictional) claims colleagues assure that our recent data set is quite homogeneous in time, there should not be strong trends.
  • New claims, obviously, have a date outside of the existing range making it hard to segment them otherwise.
  • For ClaimCateg there is no documentation, this category will be ignored.

3 Data preparation

For educational reasons, we distinguish two different settings:

  1. The data set does only contain numeric variables.
  2. The data set contains numeric and categorical variables.

3.1 Numeric features

3.1.1 Pre-processing

CHAT: Which points do we have to consider in our pre-processing?

We remove categorical variables, missings and ensure that we do not introduce any duplicates

dat_num_no_missings = unique(na.omit(dat4clustering[,mget(num_vars)]))

Here we use the Euclidean metric here. Since the numeric variables are measured on different scales (years, counts, EURs, sizes, etc.) we have to center and scale the data:

dat_num_no_missings_scaled = scale(dat_num_no_missings)

3.1.2 Descriptive analysis

First look at the data:

library(ggridges)
library(ggplot2)

data4ridgeplot <- melt(as.data.table(dat_num_no_missings_scaled))
ggplot(data4ridgeplot) +  geom_density_ridges(aes(x = value, y = variable)) + theme_ridges() + theme(legend.position = "none") + xlim(-2.5,2.5) # given a normal distribution ~1% should be outside of this range

Typically, variables with a large variance turn out to be more important differentiators. Also correlations should be checked.

YES OR NO: Do you expect large correlations?

cor_mat <- cor(dat_num_no_missings_scaled, method="spearman")
library(corrplot)
corrplot.mixed(cor_mat,order="hclust",lower.col = "black")

A combination of histograms and correlations can be obtained using a pairs plot (using Pearson correlation):

library(GGally)
Registered S3 method overwritten by 'GGally':
  method from   
  +.gg   ggplot2
dat_num_no_missings_scaled_df <- as.data.frame(dat_num_no_missings_scaled)
ggpairs(dat_num_no_missings_scaled_df[,c("ShipLength","ShipTonnage")], progress=FALSE)

Further pre-processing steps might be very well reasonable lead to different analyses (cf. this article on researchers degrees of freedom for a general account on this topic). For example:

  • Ship length and tonnage are (unsurprisingly) highly correlated. One could remove one of the variables or combine both into a new variable, e.g. length per ton (like in life insurance where one often uses BMI instead of height and weight)
  • ClaimPaid variable may be divided by Insured Value. Since these variables are not an objective measure of the ship but a result of an insurance process (underwriting and claims assessment), one may also remove them from the data set. In such a setting, one may further analyze if the clusters based on the similarity of the ships reflect different risk groups, which can be a preliminary step to making a supervised model aiming to predict paid claims based on the ship’s covariates.

In any case, domain knowledge is essential in data pre-processing.

3.2 Mixed-type Data

We’ll filter out NAs here and focus only on the clustering part. In practice, you could use mice to complete the data, but it may be a good starting point to first consider only available data.

dat_no_missings = unique(na.omit(dat4clustering[,mget(c(num_vars,cat_vars))]))

As before the numerical variables have to be scaled with scale() as above. Then we re-add the categorical features again.

dat_no_missings_scaled <- as.data.table(scale(dat_no_missings[,mget(num_vars)]))
dat_no_missings_scaled[,ShipBrand:=dat_no_missings$ShipBrand]
dat_no_missings_scaled[,ShipHull:=dat_no_missings$ShipHull]
dat_no_missings_scaled[,Departement:=dat_no_missings$Departement]

4 Clustering algorithms

4.1 K-Means

YES OR NO: Ever worked with k-means before?

4.1.1 Numeric data

We will perform a K-means Cluster Analysis using the flexclust package here. Below we run clustering algorithms repeatedly for different numbers of clusters and returns the minimum within cluster distance solution for each. The sum of within cluster distances which is decreasing with k (since between cluster distances are not counted). Ideally we’d look for an elbow / hockey stick.

library(flexclust)
k_try <-2:15
find_k <- stepFlexclust(dat_num_no_missings_scaled,k_try,nrep=5,verbose=FALSE, seed=seed_var, FUN=cclust)
plot(find_k)

Based on the result and our applications we select 7 clusters.

nr_clusters = 7
cclust_res <-  getModel(find_k,nr_clusters-1)
cclust_res
kcca object of family ‘kmeans’ 

call:
stepFlexclust(x = dat_num_no_missings_scaled, k = 7L, nrep = 5, verbose = FALSE, FUN = cclust, seed = seed_var)

cluster sizes:

  1   2   3   4   5   6   7 
221 112  55  63 190 137 143 

4.1.2 Mixed data types

One can use the clustMixType as in the first part of the course which uses the k-prototypes algorithm. Alternatively, one can use partitioning around medoids, where one computes the distance matrix using Gower’s distance a-priori. However, this approach is rather slow (and memory intensive) and works only for smaller data sets. We will show an example of a clustering based on a pre-computed distance matrix later in these notes.

4.2 Hierarchical dbscan

YES OR NO: Do you expect the result of h-dbscan to be similar to the one for k-means?

Here we are using a variant of DBSCAN, hierarchical DBSCAN. It has the benefit that we only have to specify the minPts parameter. Note that not all points are clustered, cluster zero contains all outliers but will not be a homogeneous set of similar points.

We want a cluster to contain at least 7 points, thus we set minPts=7. By playing around with this parameter, we see that a larger number produces very few clusters which would not be helpful, and a lower number leads to a lot of outliers. We observe that most points fall into cluster 2. Such a result would not be quite practical if we need segments for our claims colleagues to specialize on. However, it does provide us with a few “uncommon” examples and 48 outlier, we’ll investigate at a later stage.

library(dbscan)
hdbscan_res <- hdbscan(dat_num_no_missings_scaled, minPts = 7)
hdbscan_res
HDBSCAN clustering for 921 objects.
Parameters: minPts = 7
The clustering contains 5 cluster(s) and 48 noise points.

  0   1   2   3   4   5 
 48   8 813   9   7  36 

Available fields: cluster, minPts, cluster_scores, membership_prob, outlier_scores, hc

HDBSCAN essentially computes the hierarchy of all DBSCAN* clusterings, meaning for all (reasonable) values of eps , the radius of the epsilon neighborhood. Then it uses a stability-based extraction method to find optimal cuts in the hierarchy, thus producing a flat solution. Depending on a different value for eps, we could get more or less clusters using the dbscan() function from the same package.

plot(hdbscan_res, show_flat=T)

A higher value of eps would result in only 4 clusters where cluster 4 and 5 will be merged. A lower value will quickly lead to a fine segmentation of cluster 2 with lots of new clusters and potentially, also outliers.

The result seems to be of less use for this data - most ships fall into cluster 2, the other categories are sparsely populated.

Similarly as with k-means, data with mixed data types can per se be used with DBSCAN if the pairwise distances are computed a-priori, which is normally not feasible for large data sets.

4.3 Model-based clustering

4.3.1 Gaussian Mixture Model for numerical data

CHAT: When did Carl Friedrich Gauß define the Normal/Gaussian distribution? (answer at the end of this section)

Here we are making the assumption that each point, given the true cluster assignment, is follows a Gaussian distribution. This stochastic framework allows us to determine the clusters via Maximum Likelihood and evaluate the fit using statistical measures, such as the BIC (Bayesian Information Criterion), which is defined as nr_parameters * log(nr_observations) minus 2 times the log-likelihood.

We fit configurations with 1 to 10 clusters, hence max_clusters=10. Ensure that the number of points >> number of clusters. Most other parameters are used to control the algorithm, that we do not investigate in detail. In practice, some sensitivity analysis will quickly show if deviating from the defaults makes a difference or not. Ideally, one at least observes convergence for large values of km_iter and em_iter. If not, the algorithm may be unstable and not fit for the data at hand.

library(ClusterR)
opt_gmm = Optimal_Clusters_GMM(dat_num_no_missings_scaled, max_clusters = 10, 
                               criterion = "BIC", dist_mode = "eucl_dist", 
                               seed_mode = "random_subset", seed= seed_var,
                               km_iter = 10, em_iter = 10, var_floor = 1e-10,
                               plot_data = T)

The fit becomes better if we increase the number of clusters from one, but eventually the improvement is lower than the penalization in the BIC formula. We’ll decide to use 7 clusters since this value is quite close to the minimum and we want to have a rather low number of clusters for our applications.

gmm_res = GMM(dat_num_no_missings_scaled,gaussian_comps  =  7, dist_mode = "eucl_dist", 
              seed_mode = "random_subset", km_iter = 10, em_iter = 10, seed = seed_var)   

We define a wrapper to easily obtain predictions from our Gaussian Mixture Model (GMM)

predictGMM <- function(x,dat) {
  centroids = x$centroids
  cov = x$covariance_matrices
  w = x$weights
  return (predict_GMM(dat,centroids,cov,w))
}

The resulting clusters are more evenly distributed

pred_gmm_res <- predictGMM(gmm_res,dat_num_no_missings_scaled)
summary(factor(pred_gmm_res$cluster_labels))
  0   1   2   3   4   5   6 
 65  22  81   6 316 235 196 

Since we deal with probabilistic models, we can compute the probability that each point x belongs to cluster k. Thus one often speaks of “soft clustering”. As long as no decision is taken, each point belongs to each cluster with a certain probability. Naturally, one assigns each point to the most likely cluster. We observe that the probabilities of the most likely cluster are nicely skewed to the right, indicating a clear cluster assignment for most points.

probab_of_majority_class <- apply(pred_gmm_res$cluster_proba,1,max)
hist(probab_of_majority_class)

YES OR NO: The 7 cluster distributions have independent components?

gmm_res$covariance_matrices
          [,1]         [,2]      [,3]      [,4]       [,5]        [,6]        [,7]         [,8]
[1,] 0.7045233 0.0000000001 0.9119202 1.5981679 1.41460231 1.605640946  0.56296429  0.043127237
[2,] 2.3970460 1.1809796578 1.5502328 0.8463268 1.41567610 3.897214112 10.48325213  1.094608677
[3,] 0.1494162 0.0000000001 0.8652210 1.0080487 0.22676706 0.013508312  0.11334347  1.697885553
[4,] 0.3497369 3.2622415977 0.6673540 0.4346947 0.95265254 0.640867547  0.41243678 58.533182013
[5,] 0.1441920 0.0000000001 1.2042783 1.2350323 0.08981686 0.003363012  0.01527854  0.001554085
[6,] 0.2207778 0.0000000001 0.7247845 0.6685620 0.03142149 0.013553045  0.06685488  0.003018756
[7,] 0.6988894 0.0000000001 0.9686870 0.4843733 0.56035609 0.904982843  0.66450006  0.014623842

What appears like a non-diagonal covariance matrix, is in fact something else: Each cluster distribution lives in an dim(dat_num_no_missings_scaled)[2] dimensional space, since one needs one dimension per variable, and has a diagonal covariance matrix stated in the rows. So the answer is yes, they do have independent components.

Note that the 2nd component of many clusters has a zero variance, thus there is no variation. The 2nd component refers to the 2nd feature, the number of engines.

Gauß defined the Normal distribution in 1809 in his work “Theoria motus corporum coelestium in sectionibus conicis solem ambientium”, which you can surprisingly even buy on Amazon.

4.3.2 Mixed data types

For data sets with numerical and categorical features, we assume a multivariate distribution that is Gaussian for numerical components and follows a Dirichlet Distribution (that is a multivariate extension of the beta distribution) for categorical features.

library(MixAll)

In contrast to the previous setting with only numerical variables, the optimal number of clusters is 5.

ldata = list(dat_no_missings_scaled[,mget(cat_vars)],
             dat_no_missings_scaled[,mget(num_vars)])
lnames = c("categorical_pk_pjk","gaussian_pk_sjk")
set.seed(seed_var)
clustermix_res <- clusterMixedData(ldata, lnames, 
                                   nbCluster = 5:9, 
                                   strategy = clusterFastStrategy(),
                                   criterion = "BIC")
summary(clustermix_res)
**************************************************************
* model name =  categorical_pk_pjk 
* model name =  gaussian_pk_sjk 
* nbSample       =  789 
* nbCluster      =  5 
* lnLikelihood   =  18043.41 
* nbFreeParameter=  349 
* criterion name =  BIC 
* criterion value=  -33758.72 
**************************************************************

There is no need to re-fit the model with this package:

pred_clustermix <- clustermix_res@zi
# pred_clustermix <- clusterPredict(ldata,clustermix_res) # for new data one would need the predict function
table(pred_clustermix)
pred_clustermix
  1   2   3   4   5 
  3  21  37 242 486 

3 minute BREAK. Have a bio break if you need one, get a tea / coffee or simply a glass of water.

5 Cluster evaluation and interpretation

5.1 Cluster evaluation metrics

We’d focus on k-means and GMM here since hdbscan produced essentially only one large cluster. Moreover, cluster 0 cannot be treated as a cluster since it is merely a collection of outliers. To make a somewhat fair comparison one would have to take out all rows belonging to the outlier cluster 0 before calculating the metrics (thereby accepting that these points are really outliers).

5.1.1 Adjusted RandIndex

The RandIndex goes through all pairs of observations and counts which fraction of them are both in the same cluster or both in different ones, in the k-means or the GMM segmentation (even when the clusters are different, for such a pair the segmentation is consistent since both observations are considered similar or un-similar in each segmentation).

Rand Index Vizualization

There is a rather low consistency between the result from GMM and k-Means, as the adjusted RandIndex shows. The different assumptions lead to quite different segmentations.

external_validation(pred_gmm_res$cluster_labels,flexclust::predict(cclust_res),
                    method = "adjusted_rand_index")
[1] 0.2426295

5.1.2 Davies-Bouldin

library(clusterSim)

The definition of the Davies-Bouldin (DB) index of a segmentation is a bit involved. The dispersion (or variation) within cluster \(i\) can be defined as \(S_i = (\sum_{x \in C_i} |x-c_i|^q)^{1/q}\), where \(c_i\) denotes the centroid of the cluster. For \(q=2\) this corresponds to the standard deviation within cluster \(i\) since the centroid is defined as the mean.

A measure of distance between the centroids of two different clusters can be computed by \(R_{i,j} = (\sum_{k=1}^n |c_{i,k}-c_{j,k}|^p)^{1/p}\). For \(p=2\) this is simply the Euclidean distance between the centroids.

The separation of cluster \(i\) from all other cluster can then be computed by \(r_i = max_{j\neq i}\frac{S_i+S_j}{R_{i,j}}\), that is, for the “worst case” pairing with cluster \(i\), the ratio between the sum of within cluster dispersions and the between cluster distance.

The DB index is then defined as the average over all clusters \(DB = \frac{1}{k}\sum_{i=1}^k r_i\).

Consequently, a lower DB implies a better separation: In such a situation, all clusters are rather far apart and tightly centered around their mean.

cclust_db <- index.DB(dat_num_no_missings_scaled, flexclust::predict(cclust_res),
                      p=2, q=2)
cclust_db$DB
[1] 1.63419

There is a higher value for GMM, meaning that k-means provides a better segmentation.

gmm_db <- index.DB(dat_num_no_missings_scaled, pred_gmm_res$cluster_labels, p=2, q=2)
gmm_db$DB
[1] 2.546584

5.2 Vizualization using T-SNE

Visualization of high-dimensional data into 2 (or 3) dimension can be done via linear (Principal Component Analysis, Multidimensional Scaling) or non-linear projection methods. While non-linear methods often achieve better projections, they are computationally more costly and have several hyper parameters.

T-SNE maps a set of points from a high-dimensional space in a lower-dimensional Mapping should preserve the local neighbourhood structure of each point by minimizing Kullback-Leibler divergence between the two distributions

library(Rtsne)

5.2.1 2 dimensions

set.seed(seed_var)
tsne_out <- Rtsne(dat_num_no_missings_scaled,pca=FALSE, perplexity = 30)

Within this (non-linear) 2-dimensional projection, we see that indeed not all clusters from the k-means segmentation are terribly well separated.

data_tsne <- as.data.table(tsne_out$Y)
ggplot(data_tsne,aes(x=V1,y=V2,color=factor(predict(cclust_res)))) + geom_point() + guides(color=guide_legend(title="Cluster")) +
scale_color_brewer(palette = "Set2")

CHAT: Any ideas how to enhance this visual inspection with (quantitative) evaluation metrics?

We can also connect this with intermediate calculation results of the Davies-Bouldin index. Let us consider Cluster 4, for example.The centroid of this cluster is much closer to the centroid of Cluster 7 than to all other cluster centroids.

cclust_db$d[,4] # matrix of distances between centroids or medoids of clusters
       1        2        3        4        5        6        7 
5.851553 5.895618 6.028385 0.000000 5.953955 6.354263 3.819466 

Some points are far away from the bulk (and close to Cluster 3), hence the dispersion of this cluster is rather high compared to the other clusters (actually highest).

cclust_db$S
[1] 1.133643 1.344648 2.834189 4.761197 1.132800 1.450058 1.427963

Looking at the R matrix, Cluster 4 is indeed not so well separated from Cluster 3 and 7.

cclust_db$R
          [,1]      [,2]      [,3]      [,4]      [,5]      [,6]      [,7]
[1,]       Inf 1.1794494 0.9814852 1.0073976 1.5377024 1.8778583 1.1487073
[2,] 1.1794494       Inf 0.9367326 1.0356583 0.9044690 1.6451230 0.9420658
[3,] 0.9814852 0.9367326       Inf 1.2599372 1.0072547 0.9587183 0.9944728
[4,] 1.0073976 1.0356583 1.2599372       Inf 0.9899299 0.9774942 1.6204255
[5,] 1.5377024 0.9044690 1.0072547 0.9899299       Inf 0.9610392 0.9275255
[6,] 1.8778583 1.6451230 0.9587183 0.9774942 0.9610392       Inf 0.9867794
[7,] 1.1487073 0.9420658 0.9944728 1.6204255 0.9275255 0.9867794       Inf

All in all, in the worst case is aggregation (max of R matrix) is has a lower index than Cluster 1 and 6, for example, which visually show a rather high dispersion and are close to other clusters.

cclust_db$r
[1] 1.877858 1.645123 1.259937 1.620425 1.537702 1.877858 1.620425

With descriptive analyses one can (e.g. histograms and correlations) one can better understand the clusters and their similarity. A bad separation of some clusters can also motivate a re-clustering with a lower number of clusters (investigate clusters with an \(r\)-value that is much higher than the average, i.e., the DB index.)

Of course, we can do the same plot for GMM. We only have to define a helper function the maps the cluster object and a data table to a vector with integers, the cluster assignments per row of the data table.

predictGMM_labels <- function(gmm,newdata){
  return (predictGMM(gmm,newdata)$cluster_labels)
}
data_tsne <- as.data.table(tsne_out$Y)
ggplot(data_tsne,aes(x=V1,y=V2,color=factor(predictGMM_labels(gmm_res,dat_num_no_missings_scaled)))) + geom_point() + guides(color=guide_legend(title="Cluster")) +
  scale_color_brewer(palette = "Set3")

5.2.2 3 dimensions

set.seed(seed_var)
tsne3 <- Rtsne(dat_num_no_missings_scaled,pca=FALSE, perplexity = 30, dims = 3)
data_tsne3 <- as.data.table(tsne3$Y)
data_tsne3$cluster <- factor(predict(cclust_res))

Within this (non-linear) 2-dimensional projection, we see that indeed not all clusters from the k-means segmentation are terribly well separated.

library(plotly)

We’ll now make use of the plotly library, since additional interactivity like rotation and filtering is helpful in a 3 dimensional plot

fig <- plot_ly(data_tsne3, x = ~V1, y = ~V2, z = ~V3, color = ~cluster)
fig <- fig %>%  add_trace(type = 'scatter3d', mode='markers', text = ~cluster,hoverinfo = 'text')
fig
`arrange_()` was deprecated in dplyr 0.7.0.
Please use `arrange()` instead.
See vignette('programming') for more help

5.3 Feature Importance

CHAT: What would you expect to be the most relevant variable? Meaning that if we randomly permute this variable, the clustering changes the most.

Permutation based approach to determine the overall relevance of a feature for the segmentation. Parallels the approach taken in classification tasks.

library(FeatureImpCluster)

We observe that build and engine year have the highest importance, while claims paid is irrelevant for the clustering result.

set.seed(seed_var)
FeatureImp_cclust <- FeatureImpCluster(cclust_res,as.data.table(dat_num_no_missings_scaled))
plot(FeatureImp_cclust)

It is very interesting to see the the clusters from GMM rely on quite different variables. Here Length and Tonnage are most relevant, also InsuredValue and ClaimPaid define the segments. On the other hand, build and engine year have a very low relevance.

# Note: this code requires FeatureImpCluster version >= 0.1.5
set.seed(seed_var)
FeatureImp_gmm <- FeatureImpCluster(clusterObj = gmm_res,
                                    data = as.data.table(dat_num_no_missings_scaled),
                                    predFUN = predictGMM_labels)
plot(FeatureImp_gmm)

5.4 Interpretation using rules-based ML models

A simple supervised machine learning algorithm may be easier to interpret than a complex unsupervised algorithm. We train this model on the cluster labels.

CHAT: Which supervised ML models do you know that you would call interpretable?

C5.0 can create an initial tree model then decompose the tree structure into a set of mutually exclusive rules. These rules can then be pruned and modified into a smaller set of potentially overlapping rules. The rules can be created using the rules option.

library(C50)

In the context of machine learning, a rule is a conditional logical statement that can be attached to a predicted value. Note that the conditions have to be considered in the printed order.

set.seed(seed_var)
gmm_rule <- C5.0(x = dat_num_no_missings_scaled, 
                 y = factor(pred_gmm_res$cluster_labels), rules = TRUE)
summary(gmm_rule)

Call:
C5.0.default(x = dat_num_no_missings_scaled, y = factor(pred_gmm_res$cluster_labels), rules = TRUE)


C5.0 [Release 2.07 GPL Edition]     Fri Apr 15 14:59:45 2022
-------------------------------

Class specified by attribute `outcome'

Read 921 cases (9 attributes) from undefined.data

Rules:

Rule 1: (65, lift 14.0)
    ShipPower <= 2.174239
    ShipEngNb > -0.2844257
    ClaimPaid <= 1.552014
    ->  class 0  [0.985]

Rule 2: (11, lift 38.6)
    ShipPower > 3.393784
    ->  class 1  [0.923]

Rule 3: (5, lift 35.9)
    ShipEngNb <= -0.2844257
    ShipLength > 2.181134
    ClaimPaid > 0.5804884
    ->  class 1  [0.857]

Rule 4: (3, lift 33.5)
    ShipPower > 0.3122873
    ShipTonnage <= -0.1037781
    InsuredValue > 0.390325
    ->  class 1  [0.800]

Rule 5: (2, lift 31.4)
    ShipPower > 2.174239
    ShipEngNb > -0.2844257
    ->  class 1  [0.750]

Rule 6: (57, lift 11.2)
    ShipPower <= 0.7142217
    ShipEngNb <= -0.2844257
    ShipTonnage <= -0.1037781
    ClaimPaid > 0.07467122
    ->  class 2  [0.983]

Rule 7: (47, lift 11.1)
    ShipPower <= 0.7142217
    ShipEngNb <= -0.2844257
    ShipTonnage <= -0.3988089
    ClaimPaid > -0.008888884
    ->  class 2  [0.980]

Rule 8: (7, lift 10.1)
    ShipLength <= -0.5353749
    InsuredValue > 0.01789716
    ->  class 2  [0.889]

Rule 9: (8/1, lift 9.1)
    ShipLength > 0.3814468
    ShipTonnage <= -0.2762257
    ->  class 2  [0.800]

Rule 10: (4, lift 127.9)
    ShipLength <= 2.181134
    ShipTonnage > -0.1037781
    ClaimPaid > 0.5804884
    ->  class 3  [0.833]

Rule 11: (2, lift 115.1)
    ShipEngNb > -0.2844257
    ClaimPaid > 1.552014
    ->  class 3  [0.750]

Rule 12: (229/1, lift 2.9)
    ShipPower <= -0.1995777
    ShipEngNb <= -0.2844257
    ShipLength <= -0.161855
    InsuredValue <= -0.3455804
    ClaimPaid <= -0.09244899
    ->  class 4  [0.991]

Rule 13: (232/2, lift 2.9)
    ShipPower <= -0.130871
    ShipEngNb <= -0.2844257
    ShipLength <= -0.5353749
    InsuredValue <= 0.01789716
    ClaimPaid <= -0.008888884
    ->  class 4  [0.987]

Rule 14: (281/3, lift 2.9)
    ShipEngNb <= -0.2844257
    ShipLength <= -0.4335058
    InsuredValue <= -0.2226395
    ClaimPaid <= -0.008888884
    ->  class 4  [0.986]

Rule 15: (194/2, lift 2.9)
    ShipEngNb <= -0.2844257
    ShipLength <= -0.161855
    ShipTonnage <= -0.4652948
    InsuredValue <= -0.3455804
    ClaimPaid <= -0.09244899
    ->  class 4  [0.985]

Rule 16: (177/1, lift 3.9)
    ShipEngNb <= -0.2844257
    ShipLength > -0.4335058
    ShipLength <= 0.3814468
    InsuredValue > -0.3455804
    InsuredValue <= 0.390325
    ClaimPaid <= -0.008888884
    ->  class 5  [0.989]

Rule 17: (19, lift 3.7)
    ShipEngNb <= -0.2844257
    ShipLength > -0.161855
    ShipTonnage <= -0.1037781
    InsuredValue <= -0.3455804
    ClaimPaid <= -0.008888884
    ->  class 5  [0.952]

Rule 18: (8/1, lift 3.1)
    ShipEngNb <= -0.2844257
    ShipLength <= 0.04188319
    ShipTonnage > -0.1037781
    InsuredValue <= 0.8661773
    ->  class 5  [0.800]

Rule 19: (659/441, lift 1.3)
    ShipLength <= 0.04188319
    ->  class 5  [0.331]

Rule 20: (201/10, lift 4.4)
    ShipPower <= 3.393784
    ShipEngNb <= -0.2844257
    ShipTonnage > -0.1037781
    ClaimPaid <= 0.5804884
    ->  class 6  [0.946]

Rule 21: (139/32, lift 3.6)
    ShipPower > 0.7142217
    ShipEngNb <= -0.2844257
    ->  class 6  [0.766]

Default class: 6


Evaluation on training data (921 cases):

            Rules     
      ----------------
        No      Errors

        21   14( 1.5%)   <<


       (a)   (b)   (c)   (d)   (e)   (f)   (g)    <-classified as
      ----  ----  ----  ----  ----  ----  ----
        65                                        (a): class 0
              19                             3    (b): class 1
                    78           2     1          (c): class 2
                           6                      (d): class 3
                               314     2          (e): class 4
                     1           4   230          (f): class 5
                                       1   195    (g): class 6


    Attribute usage:

     95.01% ShipEngNb
     93.38% ClaimPaid
     75.14% ShipLength
     69.82% ShipPower
     57.87% InsuredValue
     53.64% ShipTonnage


Time: 0.0 secs

Interestingly, the attribute usage provides another source to measure variable importance. The lift values can be interpreted as an importance of the rule.

The evaluation of our algorithm should be taken with case, since we are using the training set. The performance can be much worse on a table of new ships. On our trainng set, only 14 ships are not fitted to the cluster they were originally assigned to.

Such an analysis is not limited to C.50, one could use decision tree or a GLM as well.

6 Dimensionality reduction

For production or interpretation, reducing the the problem to a lower dimensional space can be helpful. What does this mean in a cluster analysis? We will show two (out of many) options:

  1. Feature Clustering: One can cluster the features to obtain groups of similar features
  2. Feature Selection: One can select a subset of features that are most important for the clustering assignment

In any case, this allows to re-compute the clusters on a lower dimensional space.

6.1 Feature Clustering

We start by first computing the distance between the features, therefore we need to transpose the data matrix.

dist_dat <- dist(t(dat_num_no_missings_scaled), method = "euclidean")
dist_dat
              ShipPower ShipEngNb ShipEngYear ShipBuildYear ShipLength ShipTonnage InsuredValue
ShipEngNb      43.08047                                                                        
ShipEngYear    42.50162  40.96290                                                              
ShipBuildYear  39.08921  39.16008    36.65984                                                  
ShipLength     20.69804  39.67558    43.01744      40.79808                                    
ShipTonnage    22.27648  40.80277    43.31603      40.70971   15.27943                         
InsuredValue   28.93815  41.50811    42.26273      38.98007   28.44953    28.35563             
ClaimPaid      41.55550  41.73910    42.89806      42.82165   40.15746    41.09929     42.06968

Based on this distance matrix, we apply a hierarchical clustering.

hc_res <- hclust(dist_dat)
plot(hc_res)

Sine we normalized our data, all features are on the same scale, so we could aggregate features by taking the mean, for example. Thus, in order to reduce the dimension from 8 to 7, we could average Length and Tonnage to a single variable that we call ShipSize.

library(dplyr)
dat_num_aggregated <- data.frame(dat_num_no_missings_scaled)
dat_num_aggregated$ShipSize = 
  (dat_num_aggregated$ShipLength + dat_num_aggregated$ShipTonnage)/2
dat_num_aggregated <- dat_num_aggregated %>% select(!c(ShipLength,ShipTonnage))

Note that the values of this variable can not be interpreted anymore in a meaningful way. In principle, this would also allow us to cluster a ship if one of the values is missing, in this case we would simply put full weight on the other variable.

YES OR NO: Do you expect the new segmentation based on Size instead of Length & Tonnage to be quite similar to the old one such that the RandIndex comparing both segmentations is near to one, say > .9 ?

set.seed(seed_var)
cclust_org_res <- cclust(dat_num_no_missings_scaled,k=7) # eliminate impact of the random seed
set.seed(seed_var)
cclust_aggr_res <- cclust(dat_num_aggregated,k=7)

Despite a negligible reduction (improvement) in the DB index.

cclust_aggr_db <- index.DB(dat_num_aggregated, flexclust::predict(cclust_aggr_res),
                      p=2, q=2)
cclust_aggr_db$DB
[1] 1.575758
cclust_org_db <- index.DB(dat_num_no_missings_scaled, flexclust::predict(cclust_org_res),
                      p=2, q=2)
cclust_org_db$DB
[1] 1.57296

However, the segmentation differs quite a bit from the previous one based on the raw data.

external_validation(flexclust::predict(cclust_org_res),
                    flexclust::predict(cclust_aggr_res),
                    method = "adjusted_rand_index")
[1] 0.7624793

6.2 Feature Selection using Feature Importance

We used Feature Importance as a tool to better understand our clustering results. Here we benefit from it in another way: To un-select features which to not materially contribute to the clustering result.

Iterating the Feature Importance algorithm over various seeds and bootstrap distributions gives a more robust estimate of feature importance.

# randomly sample starting seeds
set.seed(seed_var)
nr_seeds <- 5
sub <- 0.7 # 70% subset in each iteration
biter <- 5 # 5 bootstrap iterations
seeds_vec <- sample(1:10000,nr_seeds)

savedImp <- data.frame(matrix(0,nr_seeds,dim(dat_num_no_missings_scaled)[2]))
count <- 1
for (s in seeds_vec) {
  set.seed(s)
  res <- cclust(dat_num_no_missings_scaled,k=nr_clusters)
  set.seed(s)
  FeatureImp_res <- FeatureImpCluster(res,as.data.table(dat_num_no_missings_scaled),sub = sub,biter = biter)
  savedImp[count,] <- FeatureImp_res$featureImp[sort(names(FeatureImp_res$featureImp))]
  count <- count + 1
}
names(savedImp) <- sort(names(FeatureImp_res$featureImp))

Turns out that feature importance is quite stable. Claims paid might be removed from the data for clustering purposes.

boxplot(savedImp)

set.seed(seed_var)
cclust_noClaimPaid_res <- cclust(data.frame(dat_num_no_missings_scaled) %>% 
                                   select(-ClaimPaid),k=7)

Again there is some difference in the resulting segmentation. It would make sense to further investiage which rows (ships) are assigned to different clusters.

external_validation(flexclust::predict(cclust_org_res),
                    flexclust::predict(cclust_noClaimPaid_res),
                    method = "adjusted_rand_index")
[1] 0.733007

6.3 Feature Selection in Model-based clustering

When fitting our Gaussian Mixture Model (GMM), we used BIC as a measure to decide which number of clusters to use (remember: higher is better). We can do the same here for variable selection.

We’ll use the clustvarsel which offers a greedy search in two directions, starting from a clustering based on a single variable (forward) or from a clustering using all variables (backward), which is what we have done before.

Greedy variable search - general algorithm:

  1. Forward initialization: Build the best univariate cluster model: Fit all GMMs with only a single variable, keep the variable \(x_{f_1}\) with the highest BIC.
  2. Backward initialization: Fit a GMM based on all variables \((x_{1},\ldots,x_{p})\)
  3. Given a fitted GMM computed on the \(m\) variables \((x_{f_1},\ldots,x_{f_m})\), perform an add and remove step and keep the model with the highest BIC, i.e., repeat the following steps:
    • Add step to determine the best larger model: Fit a GMM for all pairs \((x_{f_1},\ldots,x_{f_m},x_j)\) for all \(j\notin \{f_1,\ldots,f_m\}\) and keep the variable \(f_{m+1}:=j\) so that the new model GMM_Add has the highest BIC among all alternatives that include variables \(\{f_1,\ldots,f_m\}\).
    • Remove step to determine the best smaller model: Fit a GMM for all subsets of \(\{f_1,\ldots,f_m\}\) of size \(m-1\), and keep the model GMM_Remove with the highest BIC.
    • Decision: Among GMM, GMM_Add and GMM_Remove, return the model GMM_new with the highest BIC.
    • Stopping criterion: If GMM_new==GMM, stop the search since no improvement is possible.

clustvarsel allows several choices regarding the covariance structure of the Gaussian model, we simply use a covariance matrix that is proportional to the identity matrix here, i.e., an equal variance for each independent component. Further details can be found in the original paper. Of course, one can also compare BIC among different choices to improve model fit.

library(clustvarsel)
library(doParallel)
set.seed(seed_var)
clustvarsel_res_forward <- clustvarsel(dat_num_no_missings_scaled,G=nr_clusters,
                               search="greedy", direction=c("forward"),
                               emModels1 = "E", emModels2 = "EII", # ?mclustModelNames
                               samp = TRUE, parallel = 3)
clustvarsel_res_backward <- clustvarsel(dat_num_no_missings_scaled,G=nr_clusters,
                               search="greedy", direction=c("backward"),
                               emModels1 = "E", emModels2 = "EII", # ?mclustModelNames
                               samp = TRUE, parallel = 3)
save(clustvarsel_res_forward,clustvarsel_res_backward,file="Clustering_2\\FeatureSelectionGMM.Rdata")

Due to the rather long computation time we saved the results a-priori and load them from a file - feel free to run the computation yourself.

load("FeatureSelectionGMM.Rdata")

We can see the trajectory of the search: After having selected two variale, neither add nor remove improve the model.

clustvarsel_res_forward$steps.info

Thus the final clustering is only based on two variables. Note that these are the two most relevant variables in the C5.0 model approximating the cluster result.

clustvarsel_res_forward$subset
ClaimPaid ShipEngNb 
        8         2 

The procedure removes four variables before it stops. Note that step 4 seems redundant. This is because the implementation seems slightly different than explained above: The algorithm iterates between Add and Remove, unless one of the steps yields a model with only a single variable or the full model (thus two adds/removes in the beginning before the iteration starts). It stops when both steps where rejected.

clustvarsel_res_backward$steps.info

Thus the model ends up with four variables, extending the forward model with insured vaulue and ship power.

clustvarsel_res_backward$subset
   ShipPower    ShipEngNb InsuredValue    ClaimPaid 
           1            2            7            8 

Note that this is not well in line with the permutation feature approach, where length and tonnage seemed to be the most relevant variables. Since feature importance depends on the model and the definition of itself, results may often be quite contradicting.

Further side note: Despite the correction with the number of parameters in the BIC formula, overfitting might be an issue. Essentially, each step is a statistical test and it is well know that a large number of joint tests increase the false discovery rate if thresholds (e.g. p-values) are not adjusted.

7 Outlier detection

CHAT: In a single sentence, how would you define the term “outlier”?

7.1 Distance based (k-means)

We’ll compute the distance of each data point to its cluster centroid to identify the top 5 points that are farthest away from it, and in that sense “special”.

centers_matrix <- cclust_res@centers[predict(cclust_res,dat_num_no_missings_scaled), ]
distances <- sqrt(rowSums((dat_num_no_missings_scaled - centers_matrix)^2))
outliers <- order(distances, decreasing=T)[1:5]
print(outliers)
[1] 253 528 634 215  76

For example, data point 253 lies in cluster 4 but has a rather low insured value and a very high paid claim. That is something our claims analyst might want to analyze further.

dat_num_no_missings_scaled[outliers,]
      ShipPower  ShipEngNb ShipEngYear ShipBuildYear  ShipLength ShipTonnage InsuredValue  ClaimPaid
[1,]  1.0611906 -0.2844257  -1.0604693    0.02102725  1.80761377   1.7931457   -0.6761163 21.1685837
[2,] -0.5018873  3.5120392   0.8916138    1.02115421  2.75839177   0.3844772    1.0865760 14.6308412
[3,]  0.8859885 -0.2844257  -1.0604693   -0.16081402 -0.05998588  -0.4154304   10.3404623  1.1966049
[4,]  0.8859885 -0.2844257  -1.0604693   -0.16081402 -0.05998588  -0.4154304   10.3404623 -0.2016342
[5,]  0.8859885 -0.2844257  -1.0604693   -0.16081402 -0.05998588  -0.4154304   10.3404623 -0.1860363
predict(cclust_res,dat_num_no_missings_scaled[outliers,])
[1] 4 3 4 4 4

In our T-SNE map we can highligh in which areas our outlier are lying.

data_tsne$distances <- distances
ggplot(data_tsne,aes(x=V1,y=V2,color=distances)) + geom_point() + scale_color_gradient2()

Note that various other analysis can be made. For example, the share of outliers by cluster.

Also, the threshold “top 5” was completely arbitrary. If there are known outliers, this threshold can be calibrated in a way similar to how this is done for classification models (e.g. considering false positives / negatives on a validation set).

7.2 Using DBSCAN

CHAT: How many calculation are required to obtain the outliers in the DBSCAN segmentation ? (positive number)

DBSCAN is the only algorithm among the ones we are focusing here that has a build-in concept of outlier points. Hence no calculations are required.

outlier_dbscan <- which(hdbscan_res$cluster==0)
outlier_dbscan
 [1]  33  35  47  49  54  58  68  76  83  95 100 116 117 128 173 215 233 240 253 263 300 311 312 321 391 397 422 436 439 505 517 528 586 612
[35] 620 623 634 637 661 750 757 771 774 780 814 863 888 893

All 5 outliers from k-means are also outliers according to the DBSCAN methodology.

outliers %in% outlier_dbscan
[1] TRUE TRUE TRUE TRUE TRUE

7.3 Probabily-based (Model-based clustering)

Definition of outlier: probability of belonging to the assigned cluster is below 50% (this can happen since we have more than 2 clusters)

outlier_gmm <- which(probab_of_majority_class<.5)
outlier_gmm
[1] 321 479 655 658 666 674 705 730 859

The outliers in GMM do not include the 5 outliers from k-means

outliers %in% outlier_gmm
[1] FALSE FALSE FALSE FALSE FALSE

And only the first one is also an outlier in DBSCAN.

outlier_gmm %in% outlier_dbscan
[1]  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE

All in all, we see that there is no universal concept of an outlier. It strongly depends on the clsutering methodology and how outlier is defined based on that.

Ideally, and unsupervised outlier detection approach is used to identify observations for manual inspection. The result of this inspection is tracked so that there will be, in the future, a data base to train a supervised learning algorithm.

LS0tDQp0aXRsZTogJ0FkdmFuY2VkIENvbmNlcHRzIG9mIENsdXN0ZXJpbmcgaW4gSW5zdXJhbmNlOiBDbHVzdGVyaW5nIG9mIE1hcmluZSBMb3NzZXMnDQpvdXRwdXQ6DQogIGh0bWxfbm90ZWJvb2s6DQogICAgdG9jOiB5ZXMNCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiByZWFkYWJsZQ0KICAgIHRvY19mbG9hdDoNCiAgICAgIGNvbGxhcHNlZDogbm8NCiAgICAgIHNtb290aF9zY3JvbGw6IG5vDQogIGh0bWxfZG9jdW1lbnQ6DQogICAgdG9jOiB5ZXMNCiAgICBkZl9wcmludDogcGFnZWQNCi0tLQ0KDQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KICBib2R5ew0KICBmb250LXNpemU6IDE0cHQ7DQp9DQo8L3N0eWxlPg0KDQojIFByaW1lcg0KDQpUaGlzIGlzIGFuIGV4YW1wbGUgb24gaG93IHRvIHVzZSBjbHVzdGVyaW5nIHRlY2huaXF1ZXMgdG8gYW5hbHl6ZSBpbnN1cmFuY2UgZGF0YSwgbm90IGEgYmx1ZSBwcmludCBvciBiZXN0IHByYWN0aWNlIHRoYXQgY2FuIGJlIGZvbGxvd2VkIGZvciBhbnkgb3RoZXIgYW5hbHlzaXMuIEFueSBkYXRhIGFuYWx5c2lzLCBpbiBwYXJ0aWN1bGFyIGluIHVuc3VwZXJ2aXNlZCBsZWFybmluZywgcmVsaWVzIGhlYXZpbHkgb24gZG9tYWluIGtub3dsZWRnZSBhbmQgdGhlIGFwcGxpY2F0aW9uIGF0IGhhbmQuIEV2ZW4gZm9yIHRoaXMgZGF0YSBzZXQgc2V2ZXJhbCBvdGhlciByZWFzb25hYmxlIGFwcHJvYWNoZXMgY291bGQgaGF2ZSBiZWVuIHRha2VuIGdpdmVuIGEgZGlmZmVyZW50IHNldCBvZiBhc3N1bXB0aW9ucyBhbmQgZ29hbHMuDQoNCiMgVGhlIGRhdGFzZXQNCg0KVGhpcyBpcyBhbiBbQ0FTIGRhdGFzZXRzXShodHRwOi8vY2FzLnVxYW0uY2EvcHViL3dlYi9DQVNkYXRhc2V0cy1tYW51YWwucGRmKS4gRnJvbSB0aGUgb2ZmaWNpYWwgZGVzY3JpcHRpb246DQoNCioiVGhlIHVuaXZhcmlhdGUgZGF0YXNldCB3YXMgY29sbGVjdGVkIGJ5IGFuIHVua25vd24gRnJlbmNoIHByaXZhdGUgaW5zdXJlciBhbmQgY29tcHJpc2UgMSwyNzQgbWFyaW5lIGxvc3NlcyBiZXR3ZWVuIHRoZSBKYW51YXJ5IDIwMDMgYW5kIEp1bmUgMjAwNi4gVGhlIHN0YXR1cyBvZiB0aGUgY2xhaW0gKHNldHRsZWQgb3Igb3BlbmVkKSBpcyBkZXRlcm1pbmVkIGF0IHRoZSBlbmQgb2YgSnVuZSAyMDA2LiIqDQoNCj4gVEhVTUJTIFVQOiBXaG8gb2YgeW91IGhhcyBleHBlcmllbmNlIGluIG1hcmluZSBpbnN1cmFuY2UgYnVzaW5lc3M/DQoNCg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCiMgaW5zdGFsbC5wYWNrYWdlcygiQ0FTZGF0YXNldHMiLCByZXBvcyA9ICJodHRwOi8vY2FzLnVxYW0uY2EvcHViLyIsIHR5cGU9InNvdXJjZSIpDQpsaWJyYXJ5KENBU2RhdGFzZXRzKQ0Kc2VlZF92YXIgPC0gMDMwNTIwMjENCmBgYA0KDQoNCmBgYHtyfQ0KZGF0YShmcmVtYXJpbmUpDQpgYGANCg0KRGF0YSBwcmV2aWV3Og0KDQpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkoZGF0YS50YWJsZSkNCmNhdF92YXJzIDwtIGMoIlNoaXBCcmFuZCIsIlNoaXBIdWxsIiwiRGVwYXJ0ZW1lbnQiKQ0KbnVtX3ZhcnMgPC0gYygiU2hpcFBvd2VyIiwiU2hpcEVuZ05iIiwiU2hpcEVuZ1llYXIiLCJTaGlwQnVpbGRZZWFyIiwiU2hpcExlbmd0aCIsIlNoaXBUb25uYWdlIiwiSW5zdXJlZFZhbHVlIiwiQ2xhaW1QYWlkIikNCmRhdDRjbHVzdGVyaW5nIDwtIGFzLmRhdGEudGFibGUoZnJlbWFyaW5lKQ0KZGF0NGNsdXN0ZXJpbmcgPC0gZGF0NGNsdXN0ZXJpbmdbQ2xhaW1TdGF0dXM9PSJzZXR0bGVkIixtZ2V0KGMobnVtX3ZhcnMsY2F0X3ZhcnMpKV0NCmxpYnJhcnkoRFQpDQpkYXRhdGFibGUoZGF0NGNsdXN0ZXJpbmcsIGZpbHRlciA9ICd0b3AnLCBvcHRpb25zID0gbGlzdChwYWdlTGVuZ3RoID0gNSwgYXV0b1dpZHRoID0gVFJVRSkpDQpgYGANCg0KIyMgU3RvcnkNCg0KV2UgcHV0IHVzIGludG8gdGhlIHBvc2l0aW9uIG9mIGEgZmljdGlvbmFsIGNsYWltcyBhbmFseXN0IHRoYXQgd2FudHMgdG8gdW5kZXJzdGFuZCB0aGUgbG9zc2VzIGJldHRlciwgaGF2aW5nIHRoZSBmb2xsb3dpbmcgcXVlc3Rpb25zIC8gd2hpc2hlcyBpbiBtaW5kOg0KDQoqIEJlaW5nIG5ldyBpbiB0aGUgcG9zaXRpb24sIEkgZG8gbm90IGhhdmUgYSBnb29kIG92ZXJ2aWV3IG9mIHJlY2VudCBsb3NzZXMuIFByZXZpb3VzIGFuYWx5c2VzIHdoZXJlIGZvY3VzZWQgb24gc2luZ2xlIGNhc2VzIHJhdGhlciB0aGVuIHRoZSBvdmVyYWxsIHBvcnRmb2xpby4NCiogV2l0aCBhIHNlZ21lbnRhdGlvbiBvZiBvdXIgcmVjZW50IGxvc3NlcyBJIGNvdWxkIHRoZSBtb25pdG9yIHRoZSBsb3NzIGRldmVsb3BtZW50IG92ZXIgdGltZS4NCg0KKiBVbmZvcnR1bmF0ZWx5LCB3ZSBhcmUgbm90IGVub3VnaCBwZW9wbGUgdG8gcmV2aWV3IGVhY2ggY2xhaW0gaW4gZGVwdGguIE5ldyBjbGFpbXMgdGhhdCBkbyBub3QgZml0IHZlcnkgd2VsbCB0byB0aGUgZXhpc3Rpbmcgc2VnbWVudGF0aW9uLCBlLmcuLCBhcmUgcXVpdGUgZmFyIGF3YXkgZnJvbSBhbGwgY2VudHJvaWRzLCBjb3VsZCBiZSBtYXJrZWQgYXMgb3V0bGllcnMgYW5kIHRyaWdnZXIgYSBkZXRhaWxlZCBhc3Nlc3NtZW50Lg0KDQoqIENsYWltcyBoYW5kbGVycyB3b3VsZCBsaWtlIHRvIGdhaW4gZWZmaWNpZW5jeSBieSBzcGVjaWFsaXNpbmcgb24gY2VydGFpbiBwb3J0Zm9saW8gc2VnbWVudHMgKGxhcmdlIHNoaXBzIHZzIHNtYWxsIGV0Yy4pDQoNCiogTXkgdW5kZXJ3cml0aW5nIGNvbGxlYWd1ZXMgd291bGQgbGlrZSB0byB1bmRlcnN0YW5kIG91ciBsb3NzZXMgYmV0dGVyIGJ1dCBtYW55IG9mIHRoZW0gZG8gbm90IGhhdmUgYSBxdWFudGl0YXRpdmUgYmFja2dyb3VuZC4gV2l0aCBhIGZldyByZXByZXNlbnRhdGl2ZSBleGFtcGxlcyAoZS5nLiwgY2x1c3RlciBjZW50cm9pZHMpLCBJIGNvdWxkIGdpdmUgdGhlbSBhIHNpbXBsaWZpZWQgYnV0IGNvbXBsZXRlIG92ZXJ2aWV3IG92ZXIgb3VyIHJlY2VudCBsb3NzZXMuDQoNCg0KIyMgRGF0YSBzZXQgZGljdGlvbmFyeQ0KDQpUaGUgZGF0YSBzZXQgaXMgb2YgbWl4ZWQgZGF0YSB0eXBlOg0KDQpgYGB7cn0NCnNhcHBseShmcmVtYXJpbmUsRlVOPWNsYXNzKQ0KYGBgDQoNCkEgKHNvbWV3aGF0IGJyaWVmKSBleHBsYW5hdGlvbiBjYW4gYmUgZm91bmQgaW4gdGhlIG1hbnVhbCBhbmQgaXMgc2hvd24gYmVsb3c6DQoNCkRhdGUgdmFyaWFibGVzOiANCg0KKiBPY2N1ckRhdGU6IFRoZSBkYXkgb2YgY2xhaW0gb2NjdXJlbmNlDQoqIFJlcG9yRGF0ZTogVGhlIGRheSBvZiBjbGFpbSByZXBvcnRpbmcuU2hpcENhdGVnVGhlIGNhdGVnb3J5IG9mIHRoZSBpbnN1cmVkIHNoaXANCg0KRmFjdG9yIHZhcmlhYmxlczoNCg0KKiBTaGlwQnJhbmQ6IFRoZSBicmFuZCBvZiB0aGUgaW5zdXJlZCBzaGlwDQoqIFNoaXBIdWxsOiBUaGUgaHVsbCBvZiB0aGUgaW5zdXJlZCBzaGlwDQoqIERlcGFydGVtZW50OiBUaGUgRnJlbmNoIHJlZ2lvbiBpbiB3aGljaCB0aGUgc2hpcCBpcyBoZWFkcXVhcnRlcmVkDQoNCk51bWVyaWMgdmFyaWFibGVzOg0KDQoqIFNoaXBQb3dlcjpUaGUgcG93ZXIgb2YgdGhlIGluc3VyZWQgc2hpcA0KKiBTaGlwRW5nTmI6IFRoZSBlbmdpbmUgbnVtYmVyIG9mIHRoZSBpbnN1cmVkIHNoaXANCiogU2hpcEVuZ1llYXI6IFRoZSBlbmdpbmUgeWVhciBvZiB0aGUgaW5zdXJlZCBzaGlwDQoqIFNoaXBCdWlsZFllYXI6IFRoZSBidWlsZGluZyB5ZWFyIG9mIHRoZSBpbnN1cmVkIHNoaXANCiogU2hpcExlbmd0aDogVGhlIGxlbmd0aCBvZiB0aGUgaW5zdXJlZCBzaGlwDQoqIFNoaXBUb25uYWdlOiBUaGUgdG9ubmFnZSBvZiB0aGUgaW5zdXJlZCBzaGlwDQoqIEluc3VyZWRWYWx1ZTogVGhlIGluc3VyZWQgdmFsdWUgb2YgdGhlIGluc3VyZWQgc2hpcA0KKiBDbGFpbVBhaWQ6IFRoZSBwYWlkIGFtb3VudCAoRVVSKSBvZiB0aGUgY2xhaW0NCiogQ2xhaW1TdGF0dXM6IFRoZSBzdGF0dXMgb2YgdGhlIGNsYWltLiBVbnNldHRsZWQNCg0KDQojIyBBc3N1bXB0aW9ucw0KDQpGb3Igc2ltcGxpY2l0eSwgd2UgYXNzdW1lIHRoZSBwYWlkIGNsYWltcyBpbmNsdWRlcyB0aGUgZmluYWwgYW1vdW50IHBhaWQgdG8gdGhlIHBvbGljeWhvbGRlciBhZnRlciBkZWR1Y3RpYmxlLCByZWNvdXJzZSBldGMuIFdlIHdpbGwgYWxzbyByZXN0cmljdCBvdXIgYW5hbHlzaXMgdG8gc2V0dGxlZCBjbGFpbXMgb25seSBmb3Igd2hpY2ggd2UgaGF2ZSBjb21wbGV0ZSBhbmQgZmluYWwgaW5mb3JtYXRpb24uDQoNCkFkZGl0aW9uYWxseSwgd2UgZGVjaWRlIG5vdCB0byB1c2UgdGhlIGRhdGUgc2luY2U6DQoNCiogT3VyIChmaWN0aW9uYWwpIGNsYWltcyBjb2xsZWFndWVzIGFzc3VyZSB0aGF0IG91ciByZWNlbnQgZGF0YSBzZXQgaXMgcXVpdGUgaG9tb2dlbmVvdXMgaW4gdGltZSwgdGhlcmUgc2hvdWxkIG5vdCBiZSBzdHJvbmcgdHJlbmRzLg0KKiBOZXcgY2xhaW1zLCBvYnZpb3VzbHksIGhhdmUgYSBkYXRlIG91dHNpZGUgb2YgdGhlIGV4aXN0aW5nIHJhbmdlIG1ha2luZyBpdCBoYXJkIHRvIHNlZ21lbnQgdGhlbSBvdGhlcndpc2UuDQoqIEZvciBDbGFpbUNhdGVnIHRoZXJlIGlzIG5vIGRvY3VtZW50YXRpb24sIHRoaXMgY2F0ZWdvcnkgd2lsbCBiZSBpZ25vcmVkLg0KDQojIERhdGEgcHJlcGFyYXRpb24NCg0KRm9yIGVkdWNhdGlvbmFsIHJlYXNvbnMsIHdlIGRpc3Rpbmd1aXNoIHR3byBkaWZmZXJlbnQgc2V0dGluZ3M6DQoNCjEuIFRoZSBkYXRhIHNldCBkb2VzIG9ubHkgY29udGFpbiBudW1lcmljIHZhcmlhYmxlcy4NCjIuIFRoZSBkYXRhIHNldCBjb250YWlucyBudW1lcmljIGFuZCBjYXRlZ29yaWNhbCB2YXJpYWJsZXMuDQoNCiMjIE51bWVyaWMgZmVhdHVyZXN7LnRhYnNldH0NCg0KDQojIyMgUHJlLXByb2Nlc3NpbmcNCg0KPiBDSEFUOiBXaGljaCBwb2ludHMgZG8gd2UgaGF2ZSB0byBjb25zaWRlciBpbiBvdXIgcHJlLXByb2Nlc3Npbmc/DQoNCldlIHJlbW92ZSBjYXRlZ29yaWNhbCB2YXJpYWJsZXMsIG1pc3NpbmdzIGFuZCBlbnN1cmUgdGhhdCB3ZSBkbyBub3QgaW50cm9kdWNlIGFueSBkdXBsaWNhdGVzDQoNCmBgYHtyfQ0KZGF0X251bV9ub19taXNzaW5ncyA9IHVuaXF1ZShuYS5vbWl0KGRhdDRjbHVzdGVyaW5nWyxtZ2V0KG51bV92YXJzKV0pKQ0KYGBgDQoNCkhlcmUgd2UgdXNlIHRoZSBFdWNsaWRlYW4gbWV0cmljIGhlcmUuIFNpbmNlIHRoZSBudW1lcmljIHZhcmlhYmxlcyBhcmUgbWVhc3VyZWQgb24gZGlmZmVyZW50IHNjYWxlcyAoeWVhcnMsIGNvdW50cywgRVVScywgc2l6ZXMsIGV0Yy4pIHdlIGhhdmUgdG8gY2VudGVyIGFuZCBzY2FsZSB0aGUgZGF0YToNCg0KYGBge3J9DQpkYXRfbnVtX25vX21pc3NpbmdzX3NjYWxlZCA9IHNjYWxlKGRhdF9udW1fbm9fbWlzc2luZ3MpDQpgYGANCg0KIyMjIERlc2NyaXB0aXZlIGFuYWx5c2lzDQoNCkZpcnN0IGxvb2sgYXQgdGhlIGRhdGE6DQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeShnZ3JpZGdlcykNCmxpYnJhcnkoZ2dwbG90MikNCg0KZGF0YTRyaWRnZXBsb3QgPC0gbWVsdChhcy5kYXRhLnRhYmxlKGRhdF9udW1fbm9fbWlzc2luZ3Nfc2NhbGVkKSkNCmdncGxvdChkYXRhNHJpZGdlcGxvdCkgKyAgZ2VvbV9kZW5zaXR5X3JpZGdlcyhhZXMoeCA9IHZhbHVlLCB5ID0gdmFyaWFibGUpKSArIHRoZW1lX3JpZGdlcygpICsgdGhlbWUobGVnZW5kLnBvc2l0aW9uID0gIm5vbmUiKSArIHhsaW0oLTIuNSwyLjUpICMgZ2l2ZW4gYSBub3JtYWwgZGlzdHJpYnV0aW9uIH4xJSBzaG91bGQgYmUgb3V0c2lkZSBvZiB0aGlzIHJhbmdlDQpgYGANCg0KVHlwaWNhbGx5LCB2YXJpYWJsZXMgd2l0aCBhIGxhcmdlIHZhcmlhbmNlIHR1cm4gb3V0IHRvIGJlIG1vcmUgaW1wb3J0YW50IGRpZmZlcmVudGlhdG9ycy4gQWxzbyBjb3JyZWxhdGlvbnMgc2hvdWxkIGJlIGNoZWNrZWQuDQoNCj4gWUVTIE9SIE5POiBEbyB5b3UgZXhwZWN0IGxhcmdlIGNvcnJlbGF0aW9ucz8NCg0KYGBge3IsIGZpZy53aWR0aD0xMiwgZmlnLmhlaWdodD0xMiwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRX0NCmNvcl9tYXQgPC0gY29yKGRhdF9udW1fbm9fbWlzc2luZ3Nfc2NhbGVkLCBtZXRob2Q9InNwZWFybWFuIikNCmxpYnJhcnkoY29ycnBsb3QpDQpjb3JycGxvdC5taXhlZChjb3JfbWF0LG9yZGVyPSJoY2x1c3QiLGxvd2VyLmNvbCA9ICJibGFjayIpDQpgYGANCg0KQSBjb21iaW5hdGlvbiBvZiBoaXN0b2dyYW1zIGFuZCBjb3JyZWxhdGlvbnMgY2FuIGJlIG9idGFpbmVkIHVzaW5nIGEgcGFpcnMgcGxvdCAodXNpbmcgUGVhcnNvbiBjb3JyZWxhdGlvbik6DQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeShHR2FsbHkpDQpkYXRfbnVtX25vX21pc3NpbmdzX3NjYWxlZF9kZiA8LSBhcy5kYXRhLmZyYW1lKGRhdF9udW1fbm9fbWlzc2luZ3Nfc2NhbGVkKQ0KZ2dwYWlycyhkYXRfbnVtX25vX21pc3NpbmdzX3NjYWxlZF9kZlssYygiU2hpcExlbmd0aCIsIlNoaXBUb25uYWdlIildLCBwcm9ncmVzcz1GQUxTRSkNCmBgYA0KDQpGdXJ0aGVyIHByZS1wcm9jZXNzaW5nIHN0ZXBzIG1pZ2h0IGJlIHZlcnkgd2VsbCByZWFzb25hYmxlIGxlYWQgdG8gZGlmZmVyZW50IGFuYWx5c2VzIChjZi4gW3RoaXNdKGh0dHA6Ly93d3cuc3RhdC5jb2x1bWJpYS5lZHUvfmdlbG1hbi9yZXNlYXJjaC91bnB1Ymxpc2hlZC9wX2hhY2tpbmcucGRmKSBhcnRpY2xlIG9uIHJlc2VhcmNoZXJzIGRlZ3JlZXMgb2YgZnJlZWRvbSBmb3IgYSBnZW5lcmFsIGFjY291bnQgb24gdGhpcyB0b3BpYykuIEZvciBleGFtcGxlOg0KDQoqIFNoaXAgbGVuZ3RoIGFuZCB0b25uYWdlIGFyZSAodW5zdXJwcmlzaW5nbHkpIGhpZ2hseSBjb3JyZWxhdGVkLiBPbmUgY291bGQgcmVtb3ZlIG9uZSBvZiB0aGUgdmFyaWFibGVzIG9yIGNvbWJpbmUgYm90aCBpbnRvIGEgbmV3IHZhcmlhYmxlLCBlLmcuIGxlbmd0aCBwZXIgdG9uIChsaWtlIGluIGxpZmUgaW5zdXJhbmNlIHdoZXJlIG9uZSBvZnRlbiB1c2VzIEJNSSBpbnN0ZWFkIG9mIGhlaWdodCBhbmQgd2VpZ2h0KQ0KKiBDbGFpbVBhaWQgdmFyaWFibGUgbWF5IGJlIGRpdmlkZWQgYnkgSW5zdXJlZCBWYWx1ZS4gU2luY2UgdGhlc2UgdmFyaWFibGVzIGFyZSBub3QgYW4gb2JqZWN0aXZlIG1lYXN1cmUgb2YgdGhlIHNoaXAgYnV0IGEgcmVzdWx0IG9mIGFuIGluc3VyYW5jZSBwcm9jZXNzICh1bmRlcndyaXRpbmcgYW5kIGNsYWltcyBhc3Nlc3NtZW50KSwgb25lIG1heSBhbHNvIHJlbW92ZSB0aGVtIGZyb20gdGhlIGRhdGEgc2V0LiBJbiBzdWNoIGEgc2V0dGluZywgb25lIG1heSBmdXJ0aGVyIGFuYWx5emUgaWYgdGhlIGNsdXN0ZXJzIGJhc2VkIG9uIHRoZSBzaW1pbGFyaXR5IG9mIHRoZSBzaGlwcyByZWZsZWN0IGRpZmZlcmVudCByaXNrIGdyb3Vwcywgd2hpY2ggY2FuIGJlIGEgcHJlbGltaW5hcnkgc3RlcCB0byBtYWtpbmcgYSBzdXBlcnZpc2VkIG1vZGVsIGFpbWluZyB0byBwcmVkaWN0IHBhaWQgY2xhaW1zIGJhc2VkIG9uIHRoZSBzaGlwJ3MgY292YXJpYXRlcy4NCg0KSW4gYW55IGNhc2UsIGRvbWFpbiBrbm93bGVkZ2UgaXMgZXNzZW50aWFsIGluIGRhdGEgcHJlLXByb2Nlc3NpbmcuDQoNCg0KIyMgTWl4ZWQtdHlwZSBEYXRhIHsudGFic2V0fQ0KDQpXZSdsbCBmaWx0ZXIgb3V0IE5BcyBoZXJlIGFuZCBmb2N1cyBvbmx5IG9uIHRoZSBjbHVzdGVyaW5nIHBhcnQuIEluIHByYWN0aWNlLCB5b3UgY291bGQgdXNlIG1pY2UgIHRvIGNvbXBsZXRlIHRoZSBkYXRhLCBidXQgaXQgbWF5IGJlIGEgZ29vZCBzdGFydGluZyBwb2ludCB0byBmaXJzdCBjb25zaWRlciBvbmx5IGF2YWlsYWJsZSBkYXRhLg0KDQpgYGB7cn0NCmRhdF9ub19taXNzaW5ncyA9IHVuaXF1ZShuYS5vbWl0KGRhdDRjbHVzdGVyaW5nWyxtZ2V0KGMobnVtX3ZhcnMsY2F0X3ZhcnMpKV0pKQ0KYGBgDQoNCkFzIGJlZm9yZSB0aGUgbnVtZXJpY2FsIHZhcmlhYmxlcyBoYXZlIHRvIGJlIHNjYWxlZCB3aXRoIHNjYWxlKCkgYXMgYWJvdmUuIFRoZW4gd2UgcmUtYWRkIHRoZSBjYXRlZ29yaWNhbCBmZWF0dXJlcyBhZ2Fpbi4NCg0KYGBge3J9DQpkYXRfbm9fbWlzc2luZ3Nfc2NhbGVkIDwtIGFzLmRhdGEudGFibGUoc2NhbGUoZGF0X25vX21pc3NpbmdzWyxtZ2V0KG51bV92YXJzKV0pKQ0KZGF0X25vX21pc3NpbmdzX3NjYWxlZFssU2hpcEJyYW5kOj1kYXRfbm9fbWlzc2luZ3MkU2hpcEJyYW5kXQ0KZGF0X25vX21pc3NpbmdzX3NjYWxlZFssU2hpcEh1bGw6PWRhdF9ub19taXNzaW5ncyRTaGlwSHVsbF0NCmRhdF9ub19taXNzaW5nc19zY2FsZWRbLERlcGFydGVtZW50Oj1kYXRfbm9fbWlzc2luZ3MkRGVwYXJ0ZW1lbnRdDQpgYGANCg0KDQojIENsdXN0ZXJpbmcgYWxnb3JpdGhtcw0KDQojIyBLLU1lYW5zDQoNCj4gWUVTIE9SIE5POiBFdmVyIHdvcmtlZCB3aXRoIGstbWVhbnMgYmVmb3JlPw0KDQojIyMgTnVtZXJpYyBkYXRhDQoNCldlIHdpbGwgcGVyZm9ybSBhIEstbWVhbnMgQ2x1c3RlciBBbmFseXNpcyB1c2luZyB0aGUgZmxleGNsdXN0IHBhY2thZ2UgaGVyZS4gQmVsb3cgd2UgcnVuIGNsdXN0ZXJpbmcgYWxnb3JpdGhtcyByZXBlYXRlZGx5IGZvciBkaWZmZXJlbnQgbnVtYmVycyBvZiBjbHVzdGVycyBhbmQgcmV0dXJucyB0aGUgbWluaW11bSB3aXRoaW4gY2x1c3RlciBkaXN0YW5jZSBzb2x1dGlvbiBmb3IgZWFjaC4gVGhlIHN1bSBvZiB3aXRoaW4gY2x1c3RlciBkaXN0YW5jZXMgd2hpY2ggaXMgZGVjcmVhc2luZyB3aXRoIGsgKHNpbmNlIGJldHdlZW4gY2x1c3RlciBkaXN0YW5jZXMgYXJlIG5vdCBjb3VudGVkKS4gSWRlYWxseSB3ZSdkIGxvb2sgZm9yIGFuIGVsYm93IC8gaG9ja2V5IHN0aWNrLg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFfQ0KbGlicmFyeShmbGV4Y2x1c3QpDQprX3RyeSA8LTI6MTUNCmZpbmRfayA8LSBzdGVwRmxleGNsdXN0KGRhdF9udW1fbm9fbWlzc2luZ3Nfc2NhbGVkLGtfdHJ5LG5yZXA9NSx2ZXJib3NlPUZBTFNFLCBzZWVkPXNlZWRfdmFyLCBGVU49Y2NsdXN0KQ0KcGxvdChmaW5kX2spDQpgYGANCg0KQmFzZWQgb24gdGhlIHJlc3VsdCBhbmQgb3VyIGFwcGxpY2F0aW9ucyB3ZSBzZWxlY3QgNyBjbHVzdGVycy4NCg0KYGBge3J9DQpucl9jbHVzdGVycyA9IDcNCmNjbHVzdF9yZXMgPC0gIGdldE1vZGVsKGZpbmRfayxucl9jbHVzdGVycy0xKQ0KY2NsdXN0X3Jlcw0KYGBgDQoNCiMjIyBNaXhlZCBkYXRhIHR5cGVzDQoNCk9uZSBjYW4gdXNlIHRoZSBjbHVzdE1peFR5cGUgYXMgaW4gdGhlIFtmaXJzdCBwYXJ0IG9mIHRoZSBjb3Vyc2VdKGh0dHBzOi8vZ2l0aHViLmNvbS9vMWl2M3IvRUFBX21hdGVyaWFsL2Jsb2IvbWFpbi9DbHVzdGVyaW5nXzEvQ0FTX2NsdXN0ZXJpbmcuUm1kKSB3aGljaCB1c2VzIHRoZSBrLXByb3RvdHlwZXMgYWxnb3JpdGhtLiAgQWx0ZXJuYXRpdmVseSwgb25lIGNhbiB1c2UgcGFydGl0aW9uaW5nIGFyb3VuZCBtZWRvaWRzLCB3aGVyZSBvbmUgY29tcHV0ZXMgdGhlIGRpc3RhbmNlIG1hdHJpeCB1c2luZyBHb3dlcidzIGRpc3RhbmNlIGEtcHJpb3JpLiBIb3dldmVyLCB0aGlzIGFwcHJvYWNoIGlzIHJhdGhlciBzbG93IChhbmQgbWVtb3J5IGludGVuc2l2ZSkgYW5kIHdvcmtzIG9ubHkgZm9yIHNtYWxsZXIgZGF0YSBzZXRzLiBXZSB3aWxsIHNob3cgYW4gZXhhbXBsZSBvZiBhIGNsdXN0ZXJpbmcgYmFzZWQgb24gYSBwcmUtY29tcHV0ZWQgZGlzdGFuY2UgbWF0cml4IGxhdGVyIGluIHRoZXNlIG5vdGVzLg0KDQoNCiMjIEhpZXJhcmNoaWNhbCBkYnNjYW4NCg0KPiBZRVMgT1IgTk86IERvIHlvdSBleHBlY3QgdGhlIHJlc3VsdCBvZiBoLWRic2NhbiB0byBiZSBzaW1pbGFyIHRvIHRoZSBvbmUgZm9yIGstbWVhbnM/DQoNCkhlcmUgd2UgYXJlIHVzaW5nIGEgdmFyaWFudCBvZiBEQlNDQU4sIGhpZXJhcmNoaWNhbCBEQlNDQU4uIEl0IGhhcyB0aGUgYmVuZWZpdCB0aGF0IHdlIG9ubHkgaGF2ZSB0byBzcGVjaWZ5IHRoZSBtaW5QdHMgcGFyYW1ldGVyLiBOb3RlIHRoYXQgbm90IGFsbCBwb2ludHMgYXJlIGNsdXN0ZXJlZCwgY2x1c3RlciB6ZXJvIGNvbnRhaW5zIGFsbCBvdXRsaWVycyBidXQgd2lsbCBub3QgYmUgYSBob21vZ2VuZW91cyBzZXQgb2Ygc2ltaWxhciBwb2ludHMuIA0KDQpXZSB3YW50IGEgY2x1c3RlciB0byBjb250YWluIGF0IGxlYXN0IDcgcG9pbnRzLCB0aHVzIHdlIHNldCBtaW5QdHM9Ny4gQnkgcGxheWluZyBhcm91bmQgd2l0aCB0aGlzIHBhcmFtZXRlciwgd2Ugc2VlIHRoYXQgYSBsYXJnZXIgbnVtYmVyIHByb2R1Y2VzIHZlcnkgZmV3IGNsdXN0ZXJzIHdoaWNoIHdvdWxkIG5vdCBiZSBoZWxwZnVsLCBhbmQgYSBsb3dlciBudW1iZXIgbGVhZHMgdG8gYSBsb3Qgb2Ygb3V0bGllcnMuIFdlIG9ic2VydmUgdGhhdCBtb3N0IHBvaW50cyBmYWxsIGludG8gY2x1c3RlciAyLiBTdWNoIGEgcmVzdWx0IHdvdWxkIG5vdCBiZSBxdWl0ZSBwcmFjdGljYWwgaWYgd2UgbmVlZCBzZWdtZW50cyBmb3Igb3VyIGNsYWltcyBjb2xsZWFndWVzIHRvIHNwZWNpYWxpemUgb24uIEhvd2V2ZXIsIGl0IGRvZXMgcHJvdmlkZSB1cyB3aXRoIGEgZmV3ICJ1bmNvbW1vbiIgZXhhbXBsZXMgYW5kIDQ4IG91dGxpZXIsIHdlJ2xsIGludmVzdGlnYXRlIGF0IGEgbGF0ZXIgc3RhZ2UuDQoNCmBgYHtyfQ0KbGlicmFyeShkYnNjYW4pDQpoZGJzY2FuX3JlcyA8LSBoZGJzY2FuKGRhdF9udW1fbm9fbWlzc2luZ3Nfc2NhbGVkLCBtaW5QdHMgPSA3KQ0KaGRic2Nhbl9yZXMNCmBgYA0KDQpIREJTQ0FOIGVzc2VudGlhbGx5IGNvbXB1dGVzIHRoZSBoaWVyYXJjaHkgb2YgYWxsIERCU0NBTiogY2x1c3RlcmluZ3MsIG1lYW5pbmcgZm9yIGFsbCAocmVhc29uYWJsZSkgdmFsdWVzIG9mIGVwcyAsIHRoZSByYWRpdXMgb2YgdGhlIGVwc2lsb24gbmVpZ2hib3Job29kLiBUaGVuIGl0IHVzZXMgYSBzdGFiaWxpdHktYmFzZWQgZXh0cmFjdGlvbiBtZXRob2QgdG8gZmluZCBvcHRpbWFsIGN1dHMgaW4gdGhlIGhpZXJhcmNoeSwgdGh1cyBwcm9kdWNpbmcgYSBmbGF0IHNvbHV0aW9uLiBEZXBlbmRpbmcgb24gYSBkaWZmZXJlbnQgdmFsdWUgZm9yIGVwcywgd2UgY291bGQgZ2V0IG1vcmUgb3IgbGVzcyBjbHVzdGVycyB1c2luZyB0aGUgZGJzY2FuKCkgZnVuY3Rpb24gZnJvbSB0aGUgc2FtZSBwYWNrYWdlLg0KDQpgYGB7cn0NCnBsb3QoaGRic2Nhbl9yZXMsIHNob3dfZmxhdD1UKQ0KYGBgDQoNCkEgaGlnaGVyIHZhbHVlIG9mIGVwcyB3b3VsZCByZXN1bHQgaW4gb25seSA0IGNsdXN0ZXJzIHdoZXJlIGNsdXN0ZXIgNCBhbmQgNSB3aWxsIGJlIG1lcmdlZC4gQSBsb3dlciB2YWx1ZSB3aWxsIHF1aWNrbHkgbGVhZCB0byBhIGZpbmUgc2VnbWVudGF0aW9uIG9mIGNsdXN0ZXIgMiB3aXRoIGxvdHMgb2YgbmV3IGNsdXN0ZXJzIGFuZCBwb3RlbnRpYWxseSwgYWxzbyBvdXRsaWVycy4NCg0KVGhlIHJlc3VsdCBzZWVtcyB0byBiZSBvZiBsZXNzIHVzZSBmb3IgdGhpcyBkYXRhIC0gbW9zdCBzaGlwcyBmYWxsIGludG8gY2x1c3RlciAyLCB0aGUgb3RoZXIgY2F0ZWdvcmllcyBhcmUgc3BhcnNlbHkgcG9wdWxhdGVkLg0KDQpTaW1pbGFybHkgYXMgd2l0aCBrLW1lYW5zLCBkYXRhIHdpdGggbWl4ZWQgZGF0YSB0eXBlcyBjYW4gcGVyIHNlIGJlIHVzZWQgd2l0aCBEQlNDQU4gaWYgdGhlIHBhaXJ3aXNlIGRpc3RhbmNlcyBhcmUgY29tcHV0ZWQgYS1wcmlvcmksIHdoaWNoIGlzIG5vcm1hbGx5IG5vdCBmZWFzaWJsZSBmb3IgbGFyZ2UgZGF0YSBzZXRzLg0KDQoNCiMjIE1vZGVsLWJhc2VkIGNsdXN0ZXJpbmcNCg0KDQojIyMgR2F1c3NpYW4gTWl4dHVyZSBNb2RlbCBmb3IgbnVtZXJpY2FsIGRhdGENCg0KPiBDSEFUOiBXaGVuIGRpZCBDYXJsIEZyaWVkcmljaCBHYXXDnyBkZWZpbmUgdGhlIE5vcm1hbC9HYXVzc2lhbiBkaXN0cmlidXRpb24/IChhbnN3ZXIgYXQgdGhlIGVuZCBvZiB0aGlzIHNlY3Rpb24pDQoNCkhlcmUgd2UgYXJlIG1ha2luZyB0aGUgYXNzdW1wdGlvbiB0aGF0IGVhY2ggcG9pbnQsIGdpdmVuIHRoZSB0cnVlIGNsdXN0ZXIgYXNzaWdubWVudCwgaXMgZm9sbG93cyBhIEdhdXNzaWFuIGRpc3RyaWJ1dGlvbi4gVGhpcyBzdG9jaGFzdGljIGZyYW1ld29yayBhbGxvd3MgdXMgdG8gZGV0ZXJtaW5lIHRoZSBjbHVzdGVycyB2aWEgTWF4aW11bSBMaWtlbGlob29kIGFuZCBldmFsdWF0ZSB0aGUgZml0IHVzaW5nIHN0YXRpc3RpY2FsIG1lYXN1cmVzLCBzdWNoIGFzIHRoZSBCSUMgKEJheWVzaWFuIEluZm9ybWF0aW9uIENyaXRlcmlvbiksIHdoaWNoIGlzIGRlZmluZWQgYXMgbnJfcGFyYW1ldGVycyAqIGxvZyhucl9vYnNlcnZhdGlvbnMpIG1pbnVzIDIgdGltZXMgdGhlIGxvZy1saWtlbGlob29kLg0KDQpXZSBmaXQgY29uZmlndXJhdGlvbnMgd2l0aCAxIHRvIDEwIGNsdXN0ZXJzLCBoZW5jZSBtYXhfY2x1c3RlcnM9MTAuIEVuc3VyZSB0aGF0IHRoZSBudW1iZXIgb2YgcG9pbnRzID4+IG51bWJlciBvZiBjbHVzdGVycy4gTW9zdCBvdGhlciBwYXJhbWV0ZXJzIGFyZSB1c2VkIHRvIGNvbnRyb2wgdGhlIGFsZ29yaXRobSwgdGhhdCB3ZSBkbyBub3QgaW52ZXN0aWdhdGUgaW4gZGV0YWlsLiBJbiBwcmFjdGljZSwgc29tZSBzZW5zaXRpdml0eSBhbmFseXNpcyB3aWxsIHF1aWNrbHkgc2hvdyBpZiBkZXZpYXRpbmcgZnJvbSB0aGUgZGVmYXVsdHMgbWFrZXMgYSBkaWZmZXJlbmNlIG9yIG5vdC4gSWRlYWxseSwgb25lIGF0IGxlYXN0IG9ic2VydmVzIGNvbnZlcmdlbmNlIGZvciBsYXJnZSB2YWx1ZXMgb2Yga21faXRlciBhbmQgZW1faXRlci4gSWYgbm90LCB0aGUgYWxnb3JpdGhtIG1heSBiZSB1bnN0YWJsZSBhbmQgbm90IGZpdCBmb3IgdGhlIGRhdGEgYXQgaGFuZC4NCg0KDQpgYGB7ciwgbWVzc2FnZT1GQUxTRX0NCmxpYnJhcnkoQ2x1c3RlclIpDQpgYGANCg0KDQpgYGB7cn0NCm9wdF9nbW0gPSBPcHRpbWFsX0NsdXN0ZXJzX0dNTShkYXRfbnVtX25vX21pc3NpbmdzX3NjYWxlZCwgbWF4X2NsdXN0ZXJzID0gMTAsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNyaXRlcmlvbiA9ICJCSUMiLCBkaXN0X21vZGUgPSAiZXVjbF9kaXN0IiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VlZF9tb2RlID0gInJhbmRvbV9zdWJzZXQiLCBzZWVkPSBzZWVkX3ZhciwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBrbV9pdGVyID0gMTAsIGVtX2l0ZXIgPSAxMCwgdmFyX2Zsb29yID0gMWUtMTAsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcGxvdF9kYXRhID0gVCkNCmBgYA0KDQpUaGUgZml0IGJlY29tZXMgYmV0dGVyIGlmIHdlIGluY3JlYXNlIHRoZSBudW1iZXIgb2YgY2x1c3RlcnMgZnJvbSBvbmUsIGJ1dCBldmVudHVhbGx5IHRoZSBpbXByb3ZlbWVudCBpcyBsb3dlciB0aGFuIHRoZSBwZW5hbGl6YXRpb24gaW4gdGhlIEJJQyBmb3JtdWxhLiBXZSdsbCBkZWNpZGUgdG8gdXNlIDcgY2x1c3RlcnMgc2luY2UgdGhpcyB2YWx1ZSBpcyBxdWl0ZSBjbG9zZSB0byB0aGUgbWluaW11bSBhbmQgd2Ugd2FudCB0byBoYXZlIGEgcmF0aGVyIGxvdyBudW1iZXIgb2YgY2x1c3RlcnMgZm9yIG91ciBhcHBsaWNhdGlvbnMuDQoNCg0KYGBge3J9DQpnbW1fcmVzID0gR01NKGRhdF9udW1fbm9fbWlzc2luZ3Nfc2NhbGVkLGdhdXNzaWFuX2NvbXBzICA9ICA3LCBkaXN0X21vZGUgPSAiZXVjbF9kaXN0IiwgDQogICAgICAgICAgICAgIHNlZWRfbW9kZSA9ICJyYW5kb21fc3Vic2V0Iiwga21faXRlciA9IDEwLCBlbV9pdGVyID0gMTAsIHNlZWQgPSBzZWVkX3ZhcikgICANCmBgYA0KDQpXZSBkZWZpbmUgYSB3cmFwcGVyIHRvIGVhc2lseSBvYnRhaW4gcHJlZGljdGlvbnMgZnJvbSBvdXIgR2F1c3NpYW4gTWl4dHVyZSBNb2RlbCAoR01NKQ0KDQpgYGB7cn0NCnByZWRpY3RHTU0gPC0gZnVuY3Rpb24oeCxkYXQpIHsNCiAgY2VudHJvaWRzID0geCRjZW50cm9pZHMNCiAgY292ID0geCRjb3ZhcmlhbmNlX21hdHJpY2VzDQogIHcgPSB4JHdlaWdodHMNCiAgcmV0dXJuIChwcmVkaWN0X0dNTShkYXQsY2VudHJvaWRzLGNvdix3KSkNCn0NCmBgYA0KDQpUaGUgcmVzdWx0aW5nIGNsdXN0ZXJzIGFyZSBtb3JlIGV2ZW5seSBkaXN0cmlidXRlZCANCg0KYGBge3J9DQpwcmVkX2dtbV9yZXMgPC0gcHJlZGljdEdNTShnbW1fcmVzLGRhdF9udW1fbm9fbWlzc2luZ3Nfc2NhbGVkKQ0Kc3VtbWFyeShmYWN0b3IocHJlZF9nbW1fcmVzJGNsdXN0ZXJfbGFiZWxzKSkNCmBgYA0KDQpTaW5jZSB3ZSBkZWFsIHdpdGggcHJvYmFiaWxpc3RpYyBtb2RlbHMsIHdlIGNhbiBjb21wdXRlIHRoZSBwcm9iYWJpbGl0eSB0aGF0IGVhY2ggcG9pbnQgeCBiZWxvbmdzIHRvIGNsdXN0ZXIgay4gVGh1cyBvbmUgb2Z0ZW4gc3BlYWtzIG9mICJzb2Z0IGNsdXN0ZXJpbmciLiBBcyBsb25nIGFzIG5vIGRlY2lzaW9uIGlzIHRha2VuLCBlYWNoIHBvaW50IGJlbG9uZ3MgdG8gZWFjaCBjbHVzdGVyIHdpdGggYSBjZXJ0YWluIHByb2JhYmlsaXR5LiBOYXR1cmFsbHksIG9uZSBhc3NpZ25zIGVhY2ggcG9pbnQgdG8gdGhlIG1vc3QgbGlrZWx5IGNsdXN0ZXIuIFdlIG9ic2VydmUgdGhhdCB0aGUgcHJvYmFiaWxpdGllcyBvZiB0aGUgbW9zdCBsaWtlbHkgY2x1c3RlciBhcmUgbmljZWx5IHNrZXdlZCB0byB0aGUgcmlnaHQsIGluZGljYXRpbmcgYSBjbGVhciBjbHVzdGVyIGFzc2lnbm1lbnQgZm9yIG1vc3QgcG9pbnRzLg0KDQpgYGB7cn0NCnByb2JhYl9vZl9tYWpvcml0eV9jbGFzcyA8LSBhcHBseShwcmVkX2dtbV9yZXMkY2x1c3Rlcl9wcm9iYSwxLG1heCkNCmhpc3QocHJvYmFiX29mX21ham9yaXR5X2NsYXNzKQ0KYGBgDQoNCj4gWUVTIE9SIE5POiBUaGUgNyBjbHVzdGVyIGRpc3RyaWJ1dGlvbnMgaGF2ZSBpbmRlcGVuZGVudCBjb21wb25lbnRzPw0KDQoNCmBgYHtyfQ0KZ21tX3JlcyRjb3ZhcmlhbmNlX21hdHJpY2VzDQpgYGANCg0KV2hhdCBhcHBlYXJzIGxpa2UgYSBub24tZGlhZ29uYWwgY292YXJpYW5jZSBtYXRyaXgsIGlzIGluIGZhY3Qgc29tZXRoaW5nIGVsc2U6IEVhY2ggY2x1c3RlciBkaXN0cmlidXRpb24gbGl2ZXMgaW4gYW4gYGRpbShkYXRfbnVtX25vX21pc3NpbmdzX3NjYWxlZClbMl1gIGRpbWVuc2lvbmFsIHNwYWNlLCBzaW5jZSBvbmUgbmVlZHMgb25lIGRpbWVuc2lvbiBwZXIgdmFyaWFibGUsIGFuZCBoYXMgYSBkaWFnb25hbCBjb3ZhcmlhbmNlIG1hdHJpeCBzdGF0ZWQgaW4gdGhlIHJvd3MuIFNvIHRoZSBhbnN3ZXIgaXMgeWVzLCB0aGV5IGRvIGhhdmUgaW5kZXBlbmRlbnQgY29tcG9uZW50cy4NCg0KTm90ZSB0aGF0IHRoZSAybmQgY29tcG9uZW50IG9mIG1hbnkgY2x1c3RlcnMgaGFzIGEgemVybyB2YXJpYW5jZSwgdGh1cyB0aGVyZSBpcyBubyB2YXJpYXRpb24uIFRoZSAybmQgY29tcG9uZW50IHJlZmVycyB0byB0aGUgMm5kIGZlYXR1cmUsIHRoZSBudW1iZXIgb2YgZW5naW5lcy4NCg0KKkdhdcOfIGRlZmluZWQgdGhlIE5vcm1hbCBkaXN0cmlidXRpb24gaW4gMTgwOSBpbiBoaXMgd29yayAiVGhlb3JpYSBtb3R1cyBjb3Jwb3J1bSBjb2VsZXN0aXVtIGluIHNlY3Rpb25pYnVzIGNvbmljaXMgc29sZW0gYW1iaWVudGl1bSIsIHdoaWNoIHlvdSBjYW4gc3VycHJpc2luZ2x5IGV2ZW4gYnV5IG9uIEFtYXpvbi4qDQoNCg0KIyMjIE1peGVkIGRhdGEgdHlwZXMNCg0KRm9yIGRhdGEgc2V0cyB3aXRoIG51bWVyaWNhbCBhbmQgY2F0ZWdvcmljYWwgZmVhdHVyZXMsIHdlIGFzc3VtZSBhIG11bHRpdmFyaWF0ZSBkaXN0cmlidXRpb24gdGhhdCBpcyAgR2F1c3NpYW4gZm9yIG51bWVyaWNhbCBjb21wb25lbnRzIGFuZCBmb2xsb3dzIGEgRGlyaWNobGV0IERpc3RyaWJ1dGlvbiAodGhhdCBpcyBhIG11bHRpdmFyaWF0ZSBleHRlbnNpb24gb2YgdGhlIGJldGEgZGlzdHJpYnV0aW9uKSBmb3IgY2F0ZWdvcmljYWwgZmVhdHVyZXMuDQoNCg0KYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KE1peEFsbCkNCmBgYA0KDQpJbiBjb250cmFzdCB0byB0aGUgcHJldmlvdXMgc2V0dGluZyB3aXRoIG9ubHkgbnVtZXJpY2FsIHZhcmlhYmxlcywgdGhlIG9wdGltYWwgbnVtYmVyIG9mIGNsdXN0ZXJzIGlzIDUuDQoNCmBgYHtyfQ0KbGRhdGEgPSBsaXN0KGRhdF9ub19taXNzaW5nc19zY2FsZWRbLG1nZXQoY2F0X3ZhcnMpXSwNCiAgICAgICAgICAgICBkYXRfbm9fbWlzc2luZ3Nfc2NhbGVkWyxtZ2V0KG51bV92YXJzKV0pDQpsbmFtZXMgPSBjKCJjYXRlZ29yaWNhbF9wa19wamsiLCJnYXVzc2lhbl9wa19zamsiKQ0Kc2V0LnNlZWQoc2VlZF92YXIpDQpjbHVzdGVybWl4X3JlcyA8LSBjbHVzdGVyTWl4ZWREYXRhKGxkYXRhLCBsbmFtZXMsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuYkNsdXN0ZXIgPSA1OjksIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHJhdGVneSA9IGNsdXN0ZXJGYXN0U3RyYXRlZ3koKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY3JpdGVyaW9uID0gIkJJQyIpDQpzdW1tYXJ5KGNsdXN0ZXJtaXhfcmVzKQ0KYGBgDQpUaGVyZSBpcyBubyBuZWVkIHRvIHJlLWZpdCB0aGUgbW9kZWwgd2l0aCB0aGlzIHBhY2thZ2U6DQoNCmBgYHtyfQ0KcHJlZF9jbHVzdGVybWl4IDwtIGNsdXN0ZXJtaXhfcmVzQHppDQojIHByZWRfY2x1c3Rlcm1peCA8LSBjbHVzdGVyUHJlZGljdChsZGF0YSxjbHVzdGVybWl4X3JlcykgIyBmb3IgbmV3IGRhdGEgb25lIHdvdWxkIG5lZWQgdGhlIHByZWRpY3QgZnVuY3Rpb24NCnRhYmxlKHByZWRfY2x1c3Rlcm1peCkNCmBgYA0KDQo+IDMgbWludXRlIEJSRUFLLiBIYXZlIGEgYmlvIGJyZWFrIGlmIHlvdSBuZWVkIG9uZSwgZ2V0IGEgdGVhIC8gY29mZmVlIG9yIHNpbXBseSBhIGdsYXNzIG9mIHdhdGVyLg0KDQojIENsdXN0ZXIgZXZhbHVhdGlvbiBhbmQgaW50ZXJwcmV0YXRpb24NCg0KIyMgQ2x1c3RlciBldmFsdWF0aW9uIG1ldHJpY3MNCg0KV2UnZCBmb2N1cyBvbiBrLW1lYW5zIGFuZCBHTU0gaGVyZSBzaW5jZSBoZGJzY2FuIHByb2R1Y2VkIGVzc2VudGlhbGx5IG9ubHkgb25lIGxhcmdlIGNsdXN0ZXIuIE1vcmVvdmVyLCBjbHVzdGVyIDAgY2Fubm90IGJlIHRyZWF0ZWQgYXMgYSBjbHVzdGVyIHNpbmNlIGl0IGlzIG1lcmVseSBhIGNvbGxlY3Rpb24gb2Ygb3V0bGllcnMuIFRvIG1ha2UgYSBzb21ld2hhdCBmYWlyIGNvbXBhcmlzb24gb25lIHdvdWxkIGhhdmUgdG8gdGFrZSBvdXQgYWxsIHJvd3MgYmVsb25naW5nIHRvIHRoZSBvdXRsaWVyIGNsdXN0ZXIgMCBiZWZvcmUgY2FsY3VsYXRpbmcgdGhlIG1ldHJpY3MgKHRoZXJlYnkgYWNjZXB0aW5nIHRoYXQgdGhlc2UgcG9pbnRzIGFyZSByZWFsbHkgb3V0bGllcnMpLg0KDQoNCiMjIyBBZGp1c3RlZCBSYW5kSW5kZXgNCg0KVGhlIFJhbmRJbmRleCBnb2VzIHRocm91Z2ggYWxsIHBhaXJzIG9mIG9ic2VydmF0aW9ucyBhbmQgY291bnRzIHdoaWNoIGZyYWN0aW9uIG9mIHRoZW0gYXJlIGJvdGggaW4gdGhlIHNhbWUgY2x1c3RlciBvciBib3RoIGluIGRpZmZlcmVudCBvbmVzLCBpbiB0aGUgay1tZWFucyBvciB0aGUgR01NIHNlZ21lbnRhdGlvbiAoZXZlbiB3aGVuIHRoZSBjbHVzdGVycyBhcmUgZGlmZmVyZW50LCBmb3Igc3VjaCBhIHBhaXIgdGhlIHNlZ21lbnRhdGlvbiBpcyBjb25zaXN0ZW50IHNpbmNlIGJvdGggb2JzZXJ2YXRpb25zIGFyZSBjb25zaWRlcmVkIHNpbWlsYXIgb3IgdW4tc2ltaWxhciBpbiBlYWNoIHNlZ21lbnRhdGlvbikuDQoNCiFbUmFuZCBJbmRleCBWaXp1YWxpemF0aW9uXShSYW5kSW5kZXgucG5nKQ0KDQpUaGVyZSBpcyBhIHJhdGhlciBsb3cgY29uc2lzdGVuY3kgYmV0d2VlbiB0aGUgcmVzdWx0IGZyb20gR01NIGFuZCBrLU1lYW5zLCBhcyB0aGUgYWRqdXN0ZWQgUmFuZEluZGV4IHNob3dzLiBUaGUgZGlmZmVyZW50IGFzc3VtcHRpb25zIGxlYWQgdG8gcXVpdGUgZGlmZmVyZW50IHNlZ21lbnRhdGlvbnMuDQoNCmBgYHtyfQ0KZXh0ZXJuYWxfdmFsaWRhdGlvbihwcmVkX2dtbV9yZXMkY2x1c3Rlcl9sYWJlbHMsZmxleGNsdXN0OjpwcmVkaWN0KGNjbHVzdF9yZXMpLA0KICAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAiYWRqdXN0ZWRfcmFuZF9pbmRleCIpDQpgYGANCg0KDQojIyMgRGF2aWVzLUJvdWxkaW4NCg0KDQpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkoY2x1c3RlclNpbSkNCmBgYA0KDQpUaGUgZGVmaW5pdGlvbiBvZiB0aGUgRGF2aWVzLUJvdWxkaW4gKERCKSBpbmRleCBvZiBhIHNlZ21lbnRhdGlvbiBpcyBhIGJpdCBpbnZvbHZlZC4gVGhlIGRpc3BlcnNpb24gKG9yIHZhcmlhdGlvbikgd2l0aGluIGNsdXN0ZXIgJGkkIGNhbiBiZSBkZWZpbmVkIGFzICRTX2kgPSAoXHN1bV97eCBcaW4gQ19pfSB8eC1jX2l8XnEpXnsxL3F9JCwgd2hlcmUgJGNfaSQgZGVub3RlcyB0aGUgY2VudHJvaWQgb2YgdGhlIGNsdXN0ZXIuIEZvciAkcT0yJCB0aGlzIGNvcnJlc3BvbmRzIHRvIHRoZSBzdGFuZGFyZCBkZXZpYXRpb24gd2l0aGluIGNsdXN0ZXIgJGkkIHNpbmNlIHRoZSBjZW50cm9pZCBpcyBkZWZpbmVkIGFzIHRoZSBtZWFuLg0KDQpBIG1lYXN1cmUgb2YgZGlzdGFuY2UgYmV0d2VlbiB0aGUgY2VudHJvaWRzIG9mIHR3byBkaWZmZXJlbnQgY2x1c3RlcnMgY2FuIGJlIGNvbXB1dGVkIGJ5IA0KJFJfe2ksan0gPSAoXHN1bV97az0xfV5uIHxjX3tpLGt9LWNfe2osa318XnApXnsxL3B9JC4gRm9yICRwPTIkIHRoaXMgaXMgc2ltcGx5IHRoZSBFdWNsaWRlYW4gZGlzdGFuY2UgYmV0d2VlbiB0aGUgY2VudHJvaWRzLg0KDQpUaGUgc2VwYXJhdGlvbiBvZiBjbHVzdGVyICRpJCBmcm9tIGFsbCBvdGhlciBjbHVzdGVyIGNhbiB0aGVuIGJlIGNvbXB1dGVkIGJ5ICRyX2kgPSBtYXhfe2pcbmVxIGl9XGZyYWN7U19pK1Nfan17Ul97aSxqfX0kLCB0aGF0IGlzLCBmb3IgdGhlICJ3b3JzdCBjYXNlIiBwYWlyaW5nIHdpdGggY2x1c3RlciAkaSQsIHRoZSByYXRpbyBiZXR3ZWVuIHRoZSBzdW0gb2Ygd2l0aGluIGNsdXN0ZXIgZGlzcGVyc2lvbnMgYW5kIHRoZSBiZXR3ZWVuIGNsdXN0ZXIgZGlzdGFuY2UuDQoNClRoZSBEQiBpbmRleCBpcyB0aGVuIGRlZmluZWQgYXMgdGhlIGF2ZXJhZ2Ugb3ZlciBhbGwgY2x1c3RlcnMNCiREQiA9IFxmcmFjezF9e2t9XHN1bV97aT0xfV5rIHJfaSQuDQoNCiFbXShEQl9pbmRleC5qcGcpDQoNCkNvbnNlcXVlbnRseSwgYSBsb3dlciBEQiBpbXBsaWVzIGEgYmV0dGVyIHNlcGFyYXRpb246IEluIHN1Y2ggYSBzaXR1YXRpb24sIGFsbCBjbHVzdGVycyBhcmUgcmF0aGVyIGZhciBhcGFydCBhbmQgdGlnaHRseSBjZW50ZXJlZCBhcm91bmQgdGhlaXIgbWVhbi4NCg0KYGBge3J9DQpjY2x1c3RfZGIgPC0gaW5kZXguREIoZGF0X251bV9ub19taXNzaW5nc19zY2FsZWQsIGZsZXhjbHVzdDo6cHJlZGljdChjY2x1c3RfcmVzKSwNCiAgICAgICAgICAgICAgICAgICAgICBwPTIsIHE9MikNCmNjbHVzdF9kYiREQg0KYGBgDQoNCg0KVGhlcmUgaXMgYSBoaWdoZXIgdmFsdWUgZm9yIEdNTSwgbWVhbmluZyB0aGF0IGstbWVhbnMgcHJvdmlkZXMgYSBiZXR0ZXIgc2VnbWVudGF0aW9uLg0KDQpgYGB7cn0NCmdtbV9kYiA8LSBpbmRleC5EQihkYXRfbnVtX25vX21pc3NpbmdzX3NjYWxlZCwgcHJlZF9nbW1fcmVzJGNsdXN0ZXJfbGFiZWxzLCBwPTIsIHE9MikNCmdtbV9kYiREQg0KYGBgDQoNCiMjIFZpenVhbGl6YXRpb24gdXNpbmcgVC1TTkUNCg0KVmlzdWFsaXphdGlvbiBvZiBoaWdoLWRpbWVuc2lvbmFsIGRhdGEgaW50byAyIChvciAzKSBkaW1lbnNpb24gY2FuIGJlIGRvbmUgdmlhIGxpbmVhciAoUHJpbmNpcGFsIENvbXBvbmVudCBBbmFseXNpcywgTXVsdGlkaW1lbnNpb25hbCBTY2FsaW5nKSBvciBub24tbGluZWFyIHByb2plY3Rpb24gbWV0aG9kcy4gV2hpbGUgbm9uLWxpbmVhciBtZXRob2RzIG9mdGVuIGFjaGlldmUgYmV0dGVyIHByb2plY3Rpb25zLCB0aGV5IGFyZSBjb21wdXRhdGlvbmFsbHkgbW9yZSBjb3N0bHkgYW5kIGhhdmUgc2V2ZXJhbCBoeXBlciBwYXJhbWV0ZXJzLg0KDQpULVNORSBtYXBzIGEgc2V0IG9mIHBvaW50cyBmcm9tIGEgaGlnaC1kaW1lbnNpb25hbCBzcGFjZSBpbiBhIGxvd2VyLWRpbWVuc2lvbmFsDQpNYXBwaW5nIHNob3VsZCBwcmVzZXJ2ZSB0aGUgbG9jYWwgbmVpZ2hib3VyaG9vZCBzdHJ1Y3R1cmUgb2YgZWFjaCBwb2ludCBieSBtaW5pbWl6aW5nIEt1bGxiYWNrLUxlaWJsZXIgZGl2ZXJnZW5jZSBiZXR3ZWVuIHRoZSB0d28gZGlzdHJpYnV0aW9ucw0KDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeShSdHNuZSkNCmBgYA0KDQojIyMgMiBkaW1lbnNpb25zDQoNCmBgYHtyfQ0Kc2V0LnNlZWQoc2VlZF92YXIpDQp0c25lX291dCA8LSBSdHNuZShkYXRfbnVtX25vX21pc3NpbmdzX3NjYWxlZCxwY2E9RkFMU0UsIHBlcnBsZXhpdHkgPSAzMCkNCmBgYA0KDQpXaXRoaW4gdGhpcyAobm9uLWxpbmVhcikgMi1kaW1lbnNpb25hbCBwcm9qZWN0aW9uLCB3ZSBzZWUgdGhhdCBpbmRlZWQgbm90IGFsbCBjbHVzdGVycyBmcm9tIHRoZSBrLW1lYW5zIHNlZ21lbnRhdGlvbiBhcmUgdGVycmlibHkgd2VsbCBzZXBhcmF0ZWQuDQoNCmBgYHtyfQ0KZGF0YV90c25lIDwtIGFzLmRhdGEudGFibGUodHNuZV9vdXQkWSkNCmdncGxvdChkYXRhX3RzbmUsYWVzKHg9VjEseT1WMixjb2xvcj1mYWN0b3IocHJlZGljdChjY2x1c3RfcmVzKSkpKSArIGdlb21fcG9pbnQoKSArIGd1aWRlcyhjb2xvcj1ndWlkZV9sZWdlbmQodGl0bGU9IkNsdXN0ZXIiKSkgKw0Kc2NhbGVfY29sb3JfYnJld2VyKHBhbGV0dGUgPSAiU2V0MiIpDQpgYGANCg0KPiBDSEFUOiBBbnkgaWRlYXMgaG93IHRvIGVuaGFuY2UgdGhpcyB2aXN1YWwgaW5zcGVjdGlvbiB3aXRoIChxdWFudGl0YXRpdmUpIGV2YWx1YXRpb24gbWV0cmljcz8NCg0KV2UgY2FuIGFsc28gY29ubmVjdCB0aGlzIHdpdGggaW50ZXJtZWRpYXRlIGNhbGN1bGF0aW9uIHJlc3VsdHMgb2YgdGhlIERhdmllcy1Cb3VsZGluIGluZGV4LiBMZXQgdXMgY29uc2lkZXIgQ2x1c3RlciA0LCBmb3IgZXhhbXBsZS5UaGUgY2VudHJvaWQgb2YgdGhpcyBjbHVzdGVyIGlzIG11Y2ggY2xvc2VyIHRvIHRoZSBjZW50cm9pZCBvZiBDbHVzdGVyIDcgdGhhbiB0byBhbGwgb3RoZXIgY2x1c3RlciBjZW50cm9pZHMuIA0KDQpgYGB7cn0NCmNjbHVzdF9kYiRkWyw0XSAjIG1hdHJpeCBvZiBkaXN0YW5jZXMgYmV0d2VlbiBjZW50cm9pZHMgb3IgbWVkb2lkcyBvZiBjbHVzdGVycw0KYGBgDQoNClNvbWUgcG9pbnRzIGFyZSBmYXIgYXdheSBmcm9tIHRoZSBidWxrIChhbmQgY2xvc2UgdG8gQ2x1c3RlciAzKSwgaGVuY2UgdGhlIGRpc3BlcnNpb24gb2YgdGhpcyBjbHVzdGVyIGlzIHJhdGhlciBoaWdoIGNvbXBhcmVkIHRvIHRoZSBvdGhlciBjbHVzdGVycyAoYWN0dWFsbHkgaGlnaGVzdCkuDQoNCmBgYHtyfQ0KY2NsdXN0X2RiJFMNCmBgYA0KDQpMb29raW5nIGF0IHRoZSBSIG1hdHJpeCwgQ2x1c3RlciA0IGlzIGluZGVlZCBub3Qgc28gd2VsbCBzZXBhcmF0ZWQgZnJvbSBDbHVzdGVyIDMgYW5kIDcuDQoNCmBgYHtyfQ0KY2NsdXN0X2RiJFINCmBgYA0KQWxsIGluIGFsbCwgaW4gdGhlIHdvcnN0IGNhc2UgaXMgYWdncmVnYXRpb24gKG1heCBvZiBSIG1hdHJpeCkgaXMgaGFzIGEgbG93ZXIgaW5kZXggdGhhbiBDbHVzdGVyIDEgYW5kIDYsIGZvciBleGFtcGxlLCB3aGljaCB2aXN1YWxseSBzaG93IGEgcmF0aGVyIGhpZ2ggZGlzcGVyc2lvbiBhbmQgYXJlIGNsb3NlIHRvIG90aGVyIGNsdXN0ZXJzLg0KDQpgYGB7cn0NCmNjbHVzdF9kYiRyDQpgYGANCg0KV2l0aCBkZXNjcmlwdGl2ZSBhbmFseXNlcyBvbmUgY2FuIChlLmcuIGhpc3RvZ3JhbXMgYW5kIGNvcnJlbGF0aW9ucykgb25lIGNhbiBiZXR0ZXIgdW5kZXJzdGFuZCB0aGUgY2x1c3RlcnMgYW5kIHRoZWlyIHNpbWlsYXJpdHkuIEEgYmFkIHNlcGFyYXRpb24gb2Ygc29tZSBjbHVzdGVycyBjYW4gYWxzbyBtb3RpdmF0ZSBhIHJlLWNsdXN0ZXJpbmcgd2l0aCBhIGxvd2VyIG51bWJlciBvZiBjbHVzdGVycyAoaW52ZXN0aWdhdGUgY2x1c3RlcnMgd2l0aCBhbiAkciQtdmFsdWUgdGhhdCBpcyBtdWNoIGhpZ2hlciB0aGFuIHRoZSBhdmVyYWdlLCBpLmUuLCB0aGUgREIgaW5kZXguKQ0KDQpPZiBjb3Vyc2UsIHdlIGNhbiBkbyB0aGUgc2FtZSBwbG90IGZvciBHTU0uIFdlIG9ubHkgaGF2ZSB0byBkZWZpbmUgYSBoZWxwZXIgZnVuY3Rpb24gdGhlIG1hcHMgdGhlIGNsdXN0ZXIgb2JqZWN0IGFuZCBhIGRhdGEgdGFibGUgdG8gYSB2ZWN0b3Igd2l0aCBpbnRlZ2VycywgdGhlIGNsdXN0ZXIgYXNzaWdubWVudHMgcGVyIHJvdyBvZiB0aGUgZGF0YSB0YWJsZS4NCg0KYGBge3J9DQpwcmVkaWN0R01NX2xhYmVscyA8LSBmdW5jdGlvbihnbW0sbmV3ZGF0YSl7DQogIHJldHVybiAocHJlZGljdEdNTShnbW0sbmV3ZGF0YSkkY2x1c3Rlcl9sYWJlbHMpDQp9DQpgYGANCg0KYGBge3J9DQpkYXRhX3RzbmUgPC0gYXMuZGF0YS50YWJsZSh0c25lX291dCRZKQ0KZ2dwbG90KGRhdGFfdHNuZSxhZXMoeD1WMSx5PVYyLGNvbG9yPWZhY3RvcihwcmVkaWN0R01NX2xhYmVscyhnbW1fcmVzLGRhdF9udW1fbm9fbWlzc2luZ3Nfc2NhbGVkKSkpKSArIGdlb21fcG9pbnQoKSArIGd1aWRlcyhjb2xvcj1ndWlkZV9sZWdlbmQodGl0bGU9IkNsdXN0ZXIiKSkgKw0KICBzY2FsZV9jb2xvcl9icmV3ZXIocGFsZXR0ZSA9ICJTZXQzIikNCmBgYA0KDQojIyMgMyBkaW1lbnNpb25zDQoNCg0KYGBge3IsIHdhcm5pbmc9RkFMU0V9DQpzZXQuc2VlZChzZWVkX3ZhcikNCnRzbmUzIDwtIFJ0c25lKGRhdF9udW1fbm9fbWlzc2luZ3Nfc2NhbGVkLHBjYT1GQUxTRSwgcGVycGxleGl0eSA9IDMwLCBkaW1zID0gMykNCmRhdGFfdHNuZTMgPC0gYXMuZGF0YS50YWJsZSh0c25lMyRZKQ0KZGF0YV90c25lMyRjbHVzdGVyIDwtIGZhY3RvcihwcmVkaWN0KGNjbHVzdF9yZXMpKQ0KYGBgDQoNCldpdGhpbiB0aGlzIChub24tbGluZWFyKSAyLWRpbWVuc2lvbmFsIHByb2plY3Rpb24sIHdlIHNlZSB0aGF0IGluZGVlZCBub3QgYWxsIGNsdXN0ZXJzIGZyb20gdGhlIGstbWVhbnMgc2VnbWVudGF0aW9uIGFyZSB0ZXJyaWJseSB3ZWxsIHNlcGFyYXRlZC4NCg0KYGBge3IsIG1lc3NhZ2U9RkFMU0V9DQpsaWJyYXJ5KHBsb3RseSkNCmBgYA0KDQpXZSdsbCBub3cgbWFrZSB1c2Ugb2YgdGhlIHBsb3RseSBsaWJyYXJ5LCBzaW5jZSBhZGRpdGlvbmFsIGludGVyYWN0aXZpdHkgbGlrZSByb3RhdGlvbiBhbmQgZmlsdGVyaW5nIGlzIGhlbHBmdWwgaW4gYSAzIGRpbWVuc2lvbmFsIHBsb3QNCg0KDQpgYGB7cn0NCmZpZyA8LSBwbG90X2x5KGRhdGFfdHNuZTMsIHggPSB+VjEsIHkgPSB+VjIsIHogPSB+VjMsIGNvbG9yID0gfmNsdXN0ZXIpDQpmaWcgPC0gZmlnICU+JSAgYWRkX3RyYWNlKHR5cGUgPSAnc2NhdHRlcjNkJywgbW9kZT0nbWFya2VycycsIHRleHQgPSB+Y2x1c3Rlcixob3ZlcmluZm8gPSAndGV4dCcpDQpmaWcNCmBgYA0KDQoNCiMjIEZlYXR1cmUgSW1wb3J0YW5jZQ0KDQo+IENIQVQ6IFdoYXQgd291bGQgeW91IGV4cGVjdCB0byBiZSB0aGUgbW9zdCByZWxldmFudCB2YXJpYWJsZT8gTWVhbmluZyB0aGF0IGlmIHdlIHJhbmRvbWx5IHBlcm11dGUgdGhpcyB2YXJpYWJsZSwgdGhlIGNsdXN0ZXJpbmcgY2hhbmdlcyB0aGUgbW9zdC4NCg0KUGVybXV0YXRpb24gYmFzZWQgYXBwcm9hY2ggdG8gZGV0ZXJtaW5lIHRoZSBvdmVyYWxsIHJlbGV2YW5jZSBvZiBhIGZlYXR1cmUgZm9yIHRoZSBzZWdtZW50YXRpb24uIFBhcmFsbGVscyB0aGUgYXBwcm9hY2ggdGFrZW4gaW4gY2xhc3NpZmljYXRpb24gdGFza3MuDQoNCiFbU291cmNlOmh0dHBzOi8vd3d3Lm1vZHVsb3MuYWkvcGVybXV0YXRpb24tZmVhdHVyZS1pbXBvcnRhbmNlLWRlZXAtZGl2ZS8sIHJldHJpZXZlZCBvbiBOb3ZlbWJlciAyMywgMjAyMV0oRmVhdHVyZV9pbXBvcnRhbmNlX2Rlc2NyaXB0aW9uX2NsdXN0ZXJpbmcucG5nKQ0KDQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KEZlYXR1cmVJbXBDbHVzdGVyKQ0KYGBgDQoNCldlIG9ic2VydmUgdGhhdCBidWlsZCBhbmQgZW5naW5lIHllYXIgaGF2ZSB0aGUgaGlnaGVzdCBpbXBvcnRhbmNlLCB3aGlsZSBjbGFpbXMgcGFpZCBpcyBpcnJlbGV2YW50IGZvciB0aGUgY2x1c3RlcmluZyByZXN1bHQuDQoNCmBgYHtyfQ0Kc2V0LnNlZWQoc2VlZF92YXIpDQpGZWF0dXJlSW1wX2NjbHVzdCA8LSBGZWF0dXJlSW1wQ2x1c3RlcihjY2x1c3RfcmVzLGFzLmRhdGEudGFibGUoZGF0X251bV9ub19taXNzaW5nc19zY2FsZWQpKQ0KcGxvdChGZWF0dXJlSW1wX2NjbHVzdCkNCmBgYA0KDQpJdCBpcyB2ZXJ5IGludGVyZXN0aW5nIHRvIHNlZSB0aGUgdGhlIGNsdXN0ZXJzIGZyb20gR01NIHJlbHkgb24gcXVpdGUgZGlmZmVyZW50IHZhcmlhYmxlcy4gSGVyZSBMZW5ndGggYW5kIFRvbm5hZ2UgYXJlIG1vc3QgcmVsZXZhbnQsIGFsc28gSW5zdXJlZFZhbHVlIGFuZCBDbGFpbVBhaWQgZGVmaW5lIHRoZSBzZWdtZW50cy4gT24gdGhlIG90aGVyIGhhbmQsIGJ1aWxkIGFuZCBlbmdpbmUgeWVhciBoYXZlIGEgdmVyeSBsb3cgcmVsZXZhbmNlLg0KDQpgYGB7cn0NCiMgTm90ZTogdGhpcyBjb2RlIHJlcXVpcmVzIEZlYXR1cmVJbXBDbHVzdGVyIHZlcnNpb24gPj0gMC4xLjUNCnNldC5zZWVkKHNlZWRfdmFyKQ0KRmVhdHVyZUltcF9nbW0gPC0gRmVhdHVyZUltcENsdXN0ZXIoY2x1c3Rlck9iaiA9IGdtbV9yZXMsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkYXRhID0gYXMuZGF0YS50YWJsZShkYXRfbnVtX25vX21pc3NpbmdzX3NjYWxlZCksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwcmVkRlVOID0gcHJlZGljdEdNTV9sYWJlbHMpDQpwbG90KEZlYXR1cmVJbXBfZ21tKQ0KYGBgDQoNCg0KIyMgSW50ZXJwcmV0YXRpb24gdXNpbmcgcnVsZXMtYmFzZWQgTUwgbW9kZWxzDQoNCkEgc2ltcGxlIHN1cGVydmlzZWQgbWFjaGluZSBsZWFybmluZyBhbGdvcml0aG0gbWF5IGJlIGVhc2llciB0byBpbnRlcnByZXQgdGhhbiBhIGNvbXBsZXggdW5zdXBlcnZpc2VkIGFsZ29yaXRobS4gV2UgdHJhaW4gdGhpcyBtb2RlbCBvbiB0aGUgY2x1c3RlciBsYWJlbHMuIA0KDQo+IENIQVQ6IFdoaWNoIHN1cGVydmlzZWQgTUwgbW9kZWxzIGRvIHlvdSBrbm93IHRoYXQgeW91IHdvdWxkIGNhbGwgaW50ZXJwcmV0YWJsZT8NCg0KQzUuMCBjYW4gY3JlYXRlIGFuIGluaXRpYWwgdHJlZSBtb2RlbCB0aGVuIGRlY29tcG9zZSB0aGUgdHJlZSBzdHJ1Y3R1cmUgaW50byBhIHNldCBvZiBtdXR1YWxseSBleGNsdXNpdmUgcnVsZXMuIFRoZXNlIHJ1bGVzIGNhbiB0aGVuIGJlIHBydW5lZCBhbmQgbW9kaWZpZWQgaW50byBhIHNtYWxsZXIgc2V0IG9mIHBvdGVudGlhbGx5IG92ZXJsYXBwaW5nIHJ1bGVzLiBUaGUgcnVsZXMgY2FuIGJlIGNyZWF0ZWQgdXNpbmcgdGhlIHJ1bGVzIG9wdGlvbi4NCg0KYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KEM1MCkNCmBgYA0KDQpJbiB0aGUgY29udGV4dCBvZiBtYWNoaW5lIGxlYXJuaW5nLCBhIHJ1bGUgaXMgYSBjb25kaXRpb25hbCBsb2dpY2FsIHN0YXRlbWVudCB0aGF0IGNhbiBiZSBhdHRhY2hlZCB0byBhIHByZWRpY3RlZCB2YWx1ZS4gTm90ZSB0aGF0IHRoZSBjb25kaXRpb25zIGhhdmUgdG8gYmUgY29uc2lkZXJlZCBpbiB0aGUgcHJpbnRlZCBvcmRlci4NCg0KYGBge3J9DQpzZXQuc2VlZChzZWVkX3ZhcikNCmdtbV9ydWxlIDwtIEM1LjAoeCA9IGRhdF9udW1fbm9fbWlzc2luZ3Nfc2NhbGVkLCANCiAgICAgICAgICAgICAgICAgeSA9IGZhY3RvcihwcmVkX2dtbV9yZXMkY2x1c3Rlcl9sYWJlbHMpLCBydWxlcyA9IFRSVUUpDQpzdW1tYXJ5KGdtbV9ydWxlKQ0KYGBgDQoNCkludGVyZXN0aW5nbHksIHRoZSBhdHRyaWJ1dGUgdXNhZ2UgcHJvdmlkZXMgYW5vdGhlciBzb3VyY2UgdG8gbWVhc3VyZSB2YXJpYWJsZSBpbXBvcnRhbmNlLiBUaGUgbGlmdCB2YWx1ZXMgY2FuIGJlIGludGVycHJldGVkIGFzIGFuIGltcG9ydGFuY2Ugb2YgdGhlIHJ1bGUuDQoNClRoZSBldmFsdWF0aW9uIG9mIG91ciBhbGdvcml0aG0gc2hvdWxkIGJlIHRha2VuIHdpdGggY2FzZSwgc2luY2Ugd2UgYXJlIHVzaW5nIHRoZSB0cmFpbmluZyBzZXQuIFRoZSBwZXJmb3JtYW5jZSBjYW4gYmUgbXVjaCB3b3JzZSBvbiBhIHRhYmxlIG9mIG5ldyBzaGlwcy4gT24gb3VyIHRyYWlubmcgc2V0LCBvbmx5IDE0IHNoaXBzIGFyZSBub3QgZml0dGVkIHRvIHRoZSBjbHVzdGVyIHRoZXkgd2VyZSBvcmlnaW5hbGx5IGFzc2lnbmVkIHRvLg0KDQpTdWNoIGFuIGFuYWx5c2lzIGlzIG5vdCBsaW1pdGVkIHRvIEMuNTAsIG9uZSBjb3VsZCB1c2UgZGVjaXNpb24gdHJlZSBvciBhIEdMTSBhcyB3ZWxsLg0KDQoNCiMgRGltZW5zaW9uYWxpdHkgcmVkdWN0aW9uDQoNCkZvciBwcm9kdWN0aW9uIG9yIGludGVycHJldGF0aW9uLCByZWR1Y2luZyB0aGUgdGhlIHByb2JsZW0gdG8gYSBsb3dlciBkaW1lbnNpb25hbCBzcGFjZSBjYW4gYmUgaGVscGZ1bC4gV2hhdCBkb2VzIHRoaXMgbWVhbiBpbiBhIGNsdXN0ZXIgYW5hbHlzaXM/IFdlIHdpbGwgc2hvdyB0d28gKG91dCBvZiBtYW55KSBvcHRpb25zOg0KDQoxLiBGZWF0dXJlIENsdXN0ZXJpbmc6IE9uZSBjYW4gY2x1c3RlciB0aGUgZmVhdHVyZXMgdG8gb2J0YWluIGdyb3VwcyBvZiBzaW1pbGFyIGZlYXR1cmVzDQoyLiBGZWF0dXJlIFNlbGVjdGlvbjogT25lIGNhbiBzZWxlY3QgYSBzdWJzZXQgb2YgZmVhdHVyZXMgdGhhdCBhcmUgbW9zdCBpbXBvcnRhbnQgZm9yIHRoZSBjbHVzdGVyaW5nIGFzc2lnbm1lbnQNCg0KSW4gYW55IGNhc2UsIHRoaXMgYWxsb3dzIHRvIHJlLWNvbXB1dGUgdGhlIGNsdXN0ZXJzIG9uIGEgbG93ZXIgZGltZW5zaW9uYWwgc3BhY2UuDQoNCiMjIEZlYXR1cmUgQ2x1c3RlcmluZw0KDQpXZSBzdGFydCBieSBmaXJzdCBjb21wdXRpbmcgdGhlIGRpc3RhbmNlIGJldHdlZW4gdGhlIGZlYXR1cmVzLCB0aGVyZWZvcmUgd2UgbmVlZCB0byB0cmFuc3Bvc2UgdGhlIGRhdGEgbWF0cml4Lg0KDQpgYGB7cn0NCmRpc3RfZGF0IDwtIGRpc3QodChkYXRfbnVtX25vX21pc3NpbmdzX3NjYWxlZCksIG1ldGhvZCA9ICJldWNsaWRlYW4iKQ0KZGlzdF9kYXQNCmBgYA0KDQpCYXNlZCBvbiB0aGlzIGRpc3RhbmNlIG1hdHJpeCwgd2UgYXBwbHkgYSBoaWVyYXJjaGljYWwgY2x1c3RlcmluZy4NCg0KYGBge3J9DQpoY19yZXMgPC0gaGNsdXN0KGRpc3RfZGF0KQ0KcGxvdChoY19yZXMpDQpgYGANCg0KU2luZSB3ZSBub3JtYWxpemVkIG91ciBkYXRhLCBhbGwgZmVhdHVyZXMgYXJlIG9uIHRoZSBzYW1lIHNjYWxlLCBzbyB3ZSBjb3VsZCBhZ2dyZWdhdGUgZmVhdHVyZXMgYnkgdGFraW5nIHRoZSBtZWFuLCBmb3IgZXhhbXBsZS4gVGh1cywgaW4gb3JkZXIgdG8gcmVkdWNlIHRoZSBkaW1lbnNpb24gZnJvbSA4IHRvIDcsIHdlIGNvdWxkIGF2ZXJhZ2UgTGVuZ3RoIGFuZCBUb25uYWdlIHRvIGEgc2luZ2xlIHZhcmlhYmxlIHRoYXQgd2UgY2FsbCBTaGlwU2l6ZS4gDQoNCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeShkcGx5cikNCmBgYA0KDQoNCmBgYHtyfQ0KZGF0X251bV9hZ2dyZWdhdGVkIDwtIGRhdGEuZnJhbWUoZGF0X251bV9ub19taXNzaW5nc19zY2FsZWQpDQpkYXRfbnVtX2FnZ3JlZ2F0ZWQkU2hpcFNpemUgPSANCiAgKGRhdF9udW1fYWdncmVnYXRlZCRTaGlwTGVuZ3RoICsgZGF0X251bV9hZ2dyZWdhdGVkJFNoaXBUb25uYWdlKS8yDQpkYXRfbnVtX2FnZ3JlZ2F0ZWQgPC0gZGF0X251bV9hZ2dyZWdhdGVkICU+JSBzZWxlY3QoIWMoU2hpcExlbmd0aCxTaGlwVG9ubmFnZSkpDQpgYGANCg0KTm90ZSB0aGF0IHRoZSB2YWx1ZXMgb2YgdGhpcyB2YXJpYWJsZSBjYW4gbm90IGJlIGludGVycHJldGVkIGFueW1vcmUgaW4gYSBtZWFuaW5nZnVsIHdheS4gSW4gcHJpbmNpcGxlLCB0aGlzIHdvdWxkIGFsc28gYWxsb3cgdXMgdG8gY2x1c3RlciBhIHNoaXAgaWYgb25lIG9mIHRoZSB2YWx1ZXMgaXMgbWlzc2luZywgaW4gdGhpcyBjYXNlIHdlIHdvdWxkIHNpbXBseSBwdXQgZnVsbCB3ZWlnaHQgb24gdGhlIG90aGVyIHZhcmlhYmxlLiANCg0KPiBZRVMgT1IgTk86IERvIHlvdSBleHBlY3QgdGhlIG5ldyBzZWdtZW50YXRpb24gYmFzZWQgb24gU2l6ZSBpbnN0ZWFkIG9mIExlbmd0aCAmIFRvbm5hZ2UgdG8gYmUgcXVpdGUgc2ltaWxhciB0byB0aGUgb2xkIG9uZSBzdWNoIHRoYXQgdGhlIFJhbmRJbmRleCBjb21wYXJpbmcgYm90aCBzZWdtZW50YXRpb25zIGlzIG5lYXIgdG8gb25lLCBzYXkgPiAuOSA/DQoNCmBgYHtyfQ0Kc2V0LnNlZWQoc2VlZF92YXIpDQpjY2x1c3Rfb3JnX3JlcyA8LSBjY2x1c3QoZGF0X251bV9ub19taXNzaW5nc19zY2FsZWQsaz03KSAjIGVsaW1pbmF0ZSBpbXBhY3Qgb2YgdGhlIHJhbmRvbSBzZWVkDQpzZXQuc2VlZChzZWVkX3ZhcikNCmNjbHVzdF9hZ2dyX3JlcyA8LSBjY2x1c3QoZGF0X251bV9hZ2dyZWdhdGVkLGs9NykNCmBgYA0KDQpEZXNwaXRlIGEgbmVnbGlnaWJsZSByZWR1Y3Rpb24gKGltcHJvdmVtZW50KSBpbiB0aGUgREIgaW5kZXguIA0KDQpgYGB7cn0NCmNjbHVzdF9hZ2dyX2RiIDwtIGluZGV4LkRCKGRhdF9udW1fYWdncmVnYXRlZCwgZmxleGNsdXN0OjpwcmVkaWN0KGNjbHVzdF9hZ2dyX3JlcyksDQogICAgICAgICAgICAgICAgICAgICAgcD0yLCBxPTIpDQpjY2x1c3RfYWdncl9kYiREQg0KY2NsdXN0X29yZ19kYiA8LSBpbmRleC5EQihkYXRfbnVtX25vX21pc3NpbmdzX3NjYWxlZCwgZmxleGNsdXN0OjpwcmVkaWN0KGNjbHVzdF9vcmdfcmVzKSwNCiAgICAgICAgICAgICAgICAgICAgICBwPTIsIHE9MikNCmNjbHVzdF9vcmdfZGIkREINCmBgYA0KDQpIb3dldmVyLCB0aGUgc2VnbWVudGF0aW9uIGRpZmZlcnMgcXVpdGUgYSBiaXQgZnJvbSB0aGUgcHJldmlvdXMgb25lIGJhc2VkIG9uIHRoZSByYXcgZGF0YS4NCg0KYGBge3J9DQpleHRlcm5hbF92YWxpZGF0aW9uKGZsZXhjbHVzdDo6cHJlZGljdChjY2x1c3Rfb3JnX3JlcyksDQogICAgICAgICAgICAgICAgICAgIGZsZXhjbHVzdDo6cHJlZGljdChjY2x1c3RfYWdncl9yZXMpLA0KICAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAiYWRqdXN0ZWRfcmFuZF9pbmRleCIpDQpgYGANCg0KDQojIyBGZWF0dXJlIFNlbGVjdGlvbiB1c2luZyBGZWF0dXJlIEltcG9ydGFuY2UNCg0KV2UgdXNlZCBGZWF0dXJlIEltcG9ydGFuY2UgYXMgYSB0b29sIHRvIGJldHRlciB1bmRlcnN0YW5kIG91ciBjbHVzdGVyaW5nIHJlc3VsdHMuIEhlcmUgd2UgYmVuZWZpdCBmcm9tIGl0IGluIGFub3RoZXIgd2F5OiBUbyB1bi1zZWxlY3QgZmVhdHVyZXMgd2hpY2ggdG8gbm90IG1hdGVyaWFsbHkgY29udHJpYnV0ZSB0byB0aGUgY2x1c3RlcmluZyByZXN1bHQuDQoNCkl0ZXJhdGluZyB0aGUgRmVhdHVyZSBJbXBvcnRhbmNlIGFsZ29yaXRobSBvdmVyIHZhcmlvdXMgc2VlZHMgYW5kIGJvb3RzdHJhcCBkaXN0cmlidXRpb25zIGdpdmVzIGEgbW9yZSByb2J1c3QgZXN0aW1hdGUgb2YgZmVhdHVyZSBpbXBvcnRhbmNlLg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRX0NCiMgcmFuZG9tbHkgc2FtcGxlIHN0YXJ0aW5nIHNlZWRzDQpzZXQuc2VlZChzZWVkX3ZhcikNCm5yX3NlZWRzIDwtIDUNCnN1YiA8LSAwLjcgIyA3MCUgc3Vic2V0IGluIGVhY2ggaXRlcmF0aW9uDQpiaXRlciA8LSA1ICMgNSBib290c3RyYXAgaXRlcmF0aW9ucw0Kc2VlZHNfdmVjIDwtIHNhbXBsZSgxOjEwMDAwLG5yX3NlZWRzKQ0KDQpzYXZlZEltcCA8LSBkYXRhLmZyYW1lKG1hdHJpeCgwLG5yX3NlZWRzLGRpbShkYXRfbnVtX25vX21pc3NpbmdzX3NjYWxlZClbMl0pKQ0KY291bnQgPC0gMQ0KZm9yIChzIGluIHNlZWRzX3ZlYykgew0KICBzZXQuc2VlZChzKQ0KICByZXMgPC0gY2NsdXN0KGRhdF9udW1fbm9fbWlzc2luZ3Nfc2NhbGVkLGs9bnJfY2x1c3RlcnMpDQogIHNldC5zZWVkKHMpDQogIEZlYXR1cmVJbXBfcmVzIDwtIEZlYXR1cmVJbXBDbHVzdGVyKHJlcyxhcy5kYXRhLnRhYmxlKGRhdF9udW1fbm9fbWlzc2luZ3Nfc2NhbGVkKSxzdWIgPSBzdWIsYml0ZXIgPSBiaXRlcikNCiAgc2F2ZWRJbXBbY291bnQsXSA8LSBGZWF0dXJlSW1wX3JlcyRmZWF0dXJlSW1wW3NvcnQobmFtZXMoRmVhdHVyZUltcF9yZXMkZmVhdHVyZUltcCkpXQ0KICBjb3VudCA8LSBjb3VudCArIDENCn0NCm5hbWVzKHNhdmVkSW1wKSA8LSBzb3J0KG5hbWVzKEZlYXR1cmVJbXBfcmVzJGZlYXR1cmVJbXApKQ0KYGBgDQoNClR1cm5zIG91dCB0aGF0IGZlYXR1cmUgaW1wb3J0YW5jZSBpcyBxdWl0ZSBzdGFibGUuIENsYWltcyBwYWlkIG1pZ2h0IGJlIHJlbW92ZWQgZnJvbSB0aGUgZGF0YSBmb3IgY2x1c3RlcmluZyBwdXJwb3Nlcy4NCg0KYGBge3IsIGZpZy5oZWlnaHQgPSA1LCBmaWcud2lkdGggPSAxMX0NCmJveHBsb3Qoc2F2ZWRJbXApDQpgYGANCg0KYGBge3J9DQpzZXQuc2VlZChzZWVkX3ZhcikNCmNjbHVzdF9ub0NsYWltUGFpZF9yZXMgPC0gY2NsdXN0KGRhdGEuZnJhbWUoZGF0X251bV9ub19taXNzaW5nc19zY2FsZWQpICU+JSANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VsZWN0KC1DbGFpbVBhaWQpLGs9NykNCmBgYA0KDQpBZ2FpbiB0aGVyZSBpcyBzb21lIGRpZmZlcmVuY2UgaW4gdGhlIHJlc3VsdGluZyBzZWdtZW50YXRpb24uIEl0IHdvdWxkIG1ha2Ugc2Vuc2UgdG8gZnVydGhlciBpbnZlc3RpYWdlIHdoaWNoIHJvd3MgKHNoaXBzKSBhcmUgYXNzaWduZWQgdG8gZGlmZmVyZW50IGNsdXN0ZXJzLg0KDQpgYGB7cn0NCmV4dGVybmFsX3ZhbGlkYXRpb24oZmxleGNsdXN0OjpwcmVkaWN0KGNjbHVzdF9vcmdfcmVzKSwNCiAgICAgICAgICAgICAgICAgICAgZmxleGNsdXN0OjpwcmVkaWN0KGNjbHVzdF9ub0NsYWltUGFpZF9yZXMpLA0KICAgICAgICAgICAgICAgICAgICBtZXRob2QgPSAiYWRqdXN0ZWRfcmFuZF9pbmRleCIpDQpgYGANCg0KDQojIyBGZWF0dXJlIFNlbGVjdGlvbiBpbiBNb2RlbC1iYXNlZCBjbHVzdGVyaW5nDQoNCldoZW4gZml0dGluZyBvdXIgR2F1c3NpYW4gTWl4dHVyZSBNb2RlbCAoR01NKSwgd2UgdXNlZCBCSUMgYXMgYSBtZWFzdXJlIHRvIGRlY2lkZSB3aGljaCBudW1iZXIgb2YgY2x1c3RlcnMgdG8gdXNlIChyZW1lbWJlcjogaGlnaGVyIGlzIGJldHRlcikuIFdlIGNhbiBkbyB0aGUgc2FtZSBoZXJlIGZvciB2YXJpYWJsZSBzZWxlY3Rpb24uDQoNCldlJ2xsIHVzZSB0aGUgY2x1c3R2YXJzZWwgd2hpY2ggb2ZmZXJzIGEgZ3JlZWR5IHNlYXJjaCBpbiB0d28gZGlyZWN0aW9ucywgc3RhcnRpbmcgZnJvbSBhIGNsdXN0ZXJpbmcgYmFzZWQgb24gYSBzaW5nbGUgdmFyaWFibGUgKGZvcndhcmQpIG9yIGZyb20gYSBjbHVzdGVyaW5nIHVzaW5nIGFsbCB2YXJpYWJsZXMgKGJhY2t3YXJkKSwgd2hpY2ggaXMgd2hhdCB3ZSBoYXZlIGRvbmUgYmVmb3JlLg0KDQoqKkdyZWVkeSB2YXJpYWJsZSBzZWFyY2gqKiAtIGdlbmVyYWwgYWxnb3JpdGhtOg0KDQoxLiAqRm9yd2FyZCBpbml0aWFsaXphdGlvbio6ICBCdWlsZCB0aGUgYmVzdCB1bml2YXJpYXRlIGNsdXN0ZXIgbW9kZWw6IEZpdCBhbGwgR01NcyB3aXRoIG9ubHkgYSBzaW5nbGUgdmFyaWFibGUsIGtlZXAgdGhlIHZhcmlhYmxlICR4X3tmXzF9JCB3aXRoIHRoZSBoaWdoZXN0IEJJQy4NCjIuICpCYWNrd2FyZCBpbml0aWFsaXphdGlvbio6IEZpdCBhIEdNTSBiYXNlZCBvbiBhbGwgdmFyaWFibGVzICQoeF97MX0sXGxkb3RzLHhfe3B9KSQNCjMuIEdpdmVuIGEgZml0dGVkICpHTU0qIGNvbXB1dGVkIG9uIHRoZSAkbSQgdmFyaWFibGVzICQoeF97Zl8xfSxcbGRvdHMseF97Zl9tfSkkLCBwZXJmb3JtIGFuIGFkZCBhbmQgcmVtb3ZlIHN0ZXAgYW5kIGtlZXAgdGhlIG1vZGVsIHdpdGggdGhlIGhpZ2hlc3QgQklDLCBpLmUuLCByZXBlYXQgdGhlIGZvbGxvd2luZyBzdGVwczoNCiAgICAqICoqQWRkIHN0ZXAqKiB0byBkZXRlcm1pbmUgdGhlIGJlc3QgbGFyZ2VyIG1vZGVsOiBGaXQgYSBHTU0gZm9yIGFsbCBwYWlycyAkKHhfe2ZfMX0sXGxkb3RzLHhfe2ZfbX0seF9qKSQgZm9yIGFsbCAkalxub3RpbiBce2ZfMSxcbGRvdHMsZl9tXH0kIGFuZCBrZWVwIHRoZSB2YXJpYWJsZSAkZl97bSsxfTo9aiQgc28gdGhhdCB0aGUgbmV3IG1vZGVsICpHTU1fQWRkKiBoYXMgdGhlIGhpZ2hlc3QgQklDIGFtb25nIGFsbCBhbHRlcm5hdGl2ZXMgdGhhdCBpbmNsdWRlIHZhcmlhYmxlcyAkXHtmXzEsXGxkb3RzLGZfbVx9JC4NCiAgICAqICoqUmVtb3ZlIHN0ZXAqKiB0byBkZXRlcm1pbmUgdGhlIGJlc3Qgc21hbGxlciBtb2RlbDogRml0IGEgR01NIGZvciBhbGwgc3Vic2V0cyBvZiAkXHtmXzEsXGxkb3RzLGZfbVx9JCBvZiBzaXplICRtLTEkLCBhbmQga2VlcCB0aGUgbW9kZWwgKkdNTV9SZW1vdmUqIHdpdGggdGhlIGhpZ2hlc3QgQklDLg0KICAgICogKipEZWNpc2lvbioqOiBBbW9uZyAqR01NKiwgKkdNTV9BZGQqIGFuZCAqR01NX1JlbW92ZSosIHJldHVybiB0aGUgbW9kZWwgKkdNTV9uZXcqIHdpdGggdGhlIGhpZ2hlc3QgQklDLg0KICAgICogKipTdG9wcGluZyBjcml0ZXJpb24qKjogSWYgKkdNTV9uZXcqPT0qR01NKiwgc3RvcCB0aGUgc2VhcmNoIHNpbmNlIG5vIGltcHJvdmVtZW50IGlzIHBvc3NpYmxlLg0KDQpjbHVzdHZhcnNlbCBhbGxvd3Mgc2V2ZXJhbCBjaG9pY2VzIHJlZ2FyZGluZyB0aGUgY292YXJpYW5jZSBzdHJ1Y3R1cmUgb2YgdGhlIEdhdXNzaWFuIG1vZGVsLCB3ZSBzaW1wbHkgdXNlIGEgY292YXJpYW5jZSBtYXRyaXggdGhhdCBpcyBwcm9wb3J0aW9uYWwgdG8gdGhlIGlkZW50aXR5IG1hdHJpeCBoZXJlLCBpLmUuLCBhbiBlcXVhbCB2YXJpYW5jZSBmb3IgZWFjaCBpbmRlcGVuZGVudCBjb21wb25lbnQuIEZ1cnRoZXIgZGV0YWlscyBjYW4gYmUgZm91bmQgaW4gdGhlIFtvcmlnaW5hbCBwYXBlcl0oaHR0cHM6Ly93d3cubmNiaS5ubG0ubmloLmdvdi9wbWMvYXJ0aWNsZXMvUE1DNTA5NjczNikuIE9mIGNvdXJzZSwgb25lIGNhbiBhbHNvIGNvbXBhcmUgQklDIGFtb25nIGRpZmZlcmVudCBjaG9pY2VzIHRvIGltcHJvdmUgbW9kZWwgZml0Lg0KDQpgYGB7ciwgZXZhbD1GQUxTRX0NCmxpYnJhcnkoY2x1c3R2YXJzZWwpDQpsaWJyYXJ5KGRvUGFyYWxsZWwpDQpzZXQuc2VlZChzZWVkX3ZhcikNCmNsdXN0dmFyc2VsX3Jlc19mb3J3YXJkIDwtIGNsdXN0dmFyc2VsKGRhdF9udW1fbm9fbWlzc2luZ3Nfc2NhbGVkLEc9bnJfY2x1c3RlcnMsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VhcmNoPSJncmVlZHkiLCBkaXJlY3Rpb249YygiZm9yd2FyZCIpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGVtTW9kZWxzMSA9ICJFIiwgZW1Nb2RlbHMyID0gIkVJSSIsICMgP21jbHVzdE1vZGVsTmFtZXMNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzYW1wID0gVFJVRSwgcGFyYWxsZWwgPSAzKQ0KY2x1c3R2YXJzZWxfcmVzX2JhY2t3YXJkIDwtIGNsdXN0dmFyc2VsKGRhdF9udW1fbm9fbWlzc2luZ3Nfc2NhbGVkLEc9bnJfY2x1c3RlcnMsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VhcmNoPSJncmVlZHkiLCBkaXJlY3Rpb249YygiYmFja3dhcmQiKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBlbU1vZGVsczEgPSAiRSIsIGVtTW9kZWxzMiA9ICJFSUkiLCAjID9tY2x1c3RNb2RlbE5hbWVzDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2FtcCA9IFRSVUUsIHBhcmFsbGVsID0gMykNCnNhdmUoY2x1c3R2YXJzZWxfcmVzX2ZvcndhcmQsY2x1c3R2YXJzZWxfcmVzX2JhY2t3YXJkLGZpbGU9IkNsdXN0ZXJpbmdfMlxcRmVhdHVyZVNlbGVjdGlvbkdNTS5SZGF0YSIpDQpgYGANCg0KRHVlIHRvIHRoZSByYXRoZXIgbG9uZyBjb21wdXRhdGlvbiB0aW1lIHdlIHNhdmVkIHRoZSByZXN1bHRzIGEtcHJpb3JpIGFuZCBsb2FkIHRoZW0gZnJvbSBhIGZpbGUgLSBmZWVsIGZyZWUgdG8gcnVuIHRoZSBjb21wdXRhdGlvbiB5b3Vyc2VsZi4gDQoNCmBgYHtyfQ0KbG9hZCgiRmVhdHVyZVNlbGVjdGlvbkdNTS5SZGF0YSIpDQpgYGANCg0KV2UgY2FuIHNlZSB0aGUgdHJhamVjdG9yeSBvZiB0aGUgc2VhcmNoOiBBZnRlciBoYXZpbmcgc2VsZWN0ZWQgdHdvIHZhcmlhbGUsIG5laXRoZXIgYWRkIG5vciByZW1vdmUgaW1wcm92ZSB0aGUgbW9kZWwuDQoNCmBgYHtyLCB3YXJuaW5nPUZBTFNFfQ0KY2x1c3R2YXJzZWxfcmVzX2ZvcndhcmQkc3RlcHMuaW5mbw0KYGBgDQpUaHVzIHRoZSBmaW5hbCBjbHVzdGVyaW5nIGlzIG9ubHkgYmFzZWQgb24gdHdvIHZhcmlhYmxlcy4gTm90ZSB0aGF0IHRoZXNlIGFyZSB0aGUgdHdvIG1vc3QgcmVsZXZhbnQgdmFyaWFibGVzIGluIHRoZSBDNS4wIG1vZGVsIGFwcHJveGltYXRpbmcgdGhlIGNsdXN0ZXIgcmVzdWx0Lg0KDQpgYGB7ciB3YXJuaW5nPUZBTFNFfQ0KY2x1c3R2YXJzZWxfcmVzX2ZvcndhcmQkc3Vic2V0DQpgYGANCg0KVGhlIHByb2NlZHVyZSByZW1vdmVzIGZvdXIgdmFyaWFibGVzIGJlZm9yZSBpdCBzdG9wcy4gTm90ZSB0aGF0IHN0ZXAgNCBzZWVtcyByZWR1bmRhbnQuIFRoaXMgaXMgYmVjYXVzZSB0aGUgaW1wbGVtZW50YXRpb24gc2VlbXMgc2xpZ2h0bHkgZGlmZmVyZW50IHRoYW4gZXhwbGFpbmVkIGFib3ZlOiBUaGUgYWxnb3JpdGhtIGl0ZXJhdGVzIGJldHdlZW4gQWRkIGFuZCBSZW1vdmUsIHVubGVzcyBvbmUgb2YgdGhlIHN0ZXBzIHlpZWxkcyBhIG1vZGVsIHdpdGggb25seSBhIHNpbmdsZSB2YXJpYWJsZSBvciB0aGUgZnVsbCBtb2RlbCAodGh1cyB0d28gYWRkcy9yZW1vdmVzIGluIHRoZSBiZWdpbm5pbmcgYmVmb3JlIHRoZSBpdGVyYXRpb24gc3RhcnRzKS4gSXQgc3RvcHMgd2hlbiBib3RoIHN0ZXBzIHdoZXJlIHJlamVjdGVkLg0KDQpgYGB7ciwgd2FybmluZz1GQUxTRX0NCmNsdXN0dmFyc2VsX3Jlc19iYWNrd2FyZCRzdGVwcy5pbmZvDQpgYGANCg0KVGh1cyB0aGUgbW9kZWwgZW5kcyB1cCB3aXRoIGZvdXIgdmFyaWFibGVzLCBleHRlbmRpbmcgdGhlIGZvcndhcmQgbW9kZWwgd2l0aCBpbnN1cmVkIHZhdWx1ZSBhbmQgc2hpcCBwb3dlci4NCg0KYGBge3IsIHdhcm5pbmc9RkFMU0V9DQpjbHVzdHZhcnNlbF9yZXNfYmFja3dhcmQkc3Vic2V0DQpgYGANCg0KTm90ZSB0aGF0IHRoaXMgaXMgbm90IHdlbGwgaW4gbGluZSB3aXRoIHRoZSBwZXJtdXRhdGlvbiBmZWF0dXJlIGFwcHJvYWNoLCB3aGVyZSBsZW5ndGggYW5kIHRvbm5hZ2Ugc2VlbWVkIHRvIGJlIHRoZSBtb3N0IHJlbGV2YW50IHZhcmlhYmxlcy4gU2luY2UgZmVhdHVyZSBpbXBvcnRhbmNlIGRlcGVuZHMgb24gdGhlIG1vZGVsIGFuZCB0aGUgZGVmaW5pdGlvbiBvZiBpdHNlbGYsIHJlc3VsdHMgbWF5IG9mdGVuIGJlIHF1aXRlIGNvbnRyYWRpY3RpbmcuIA0KDQpGdXJ0aGVyIHNpZGUgbm90ZTogRGVzcGl0ZSB0aGUgY29ycmVjdGlvbiB3aXRoIHRoZSBudW1iZXIgb2YgcGFyYW1ldGVycyBpbiB0aGUgQklDIGZvcm11bGEsIG92ZXJmaXR0aW5nIG1pZ2h0IGJlIGFuIGlzc3VlLiBFc3NlbnRpYWxseSwgZWFjaCBzdGVwIGlzIGEgc3RhdGlzdGljYWwgdGVzdCBhbmQgaXQgaXMgd2VsbCBrbm93IHRoYXQgYSBsYXJnZSBudW1iZXIgb2Ygam9pbnQgdGVzdHMgaW5jcmVhc2UgdGhlIGZhbHNlIGRpc2NvdmVyeSByYXRlIGlmIHRocmVzaG9sZHMgKGUuZy4gcC12YWx1ZXMpIGFyZSBub3QgYWRqdXN0ZWQuDQoNCiMgT3V0bGllciBkZXRlY3Rpb24NCg0KPiBDSEFUOiBJbiBhIHNpbmdsZSBzZW50ZW5jZSwgaG93IHdvdWxkIHlvdSBkZWZpbmUgdGhlIHRlcm0gIm91dGxpZXIiPw0KDQojIyBEaXN0YW5jZSBiYXNlZCAoay1tZWFucykNCg0KV2UnbGwgY29tcHV0ZSB0aGUgZGlzdGFuY2Ugb2YgZWFjaCBkYXRhIHBvaW50IHRvIGl0cyBjbHVzdGVyIGNlbnRyb2lkIHRvIGlkZW50aWZ5IHRoZSB0b3AgNSBwb2ludHMgdGhhdCBhcmUgZmFydGhlc3QgYXdheSBmcm9tIGl0LCBhbmQgaW4gdGhhdCBzZW5zZSAic3BlY2lhbCIuDQoNCmBgYHtyfQ0KY2VudGVyc19tYXRyaXggPC0gY2NsdXN0X3Jlc0BjZW50ZXJzW3ByZWRpY3QoY2NsdXN0X3JlcyxkYXRfbnVtX25vX21pc3NpbmdzX3NjYWxlZCksIF0NCmRpc3RhbmNlcyA8LSBzcXJ0KHJvd1N1bXMoKGRhdF9udW1fbm9fbWlzc2luZ3Nfc2NhbGVkIC0gY2VudGVyc19tYXRyaXgpXjIpKQ0Kb3V0bGllcnMgPC0gb3JkZXIoZGlzdGFuY2VzLCBkZWNyZWFzaW5nPVQpWzE6NV0NCmBgYA0KDQoNCmBgYHtyfQ0KcHJpbnQob3V0bGllcnMpDQpgYGANCg0KRm9yIGV4YW1wbGUsIGRhdGEgcG9pbnQgMjUzIGxpZXMgaW4gY2x1c3RlciA0IGJ1dCBoYXMgYSByYXRoZXIgbG93IGluc3VyZWQgdmFsdWUgYW5kIGEgdmVyeSBoaWdoIHBhaWQgY2xhaW0uIFRoYXQgaXMgc29tZXRoaW5nIG91ciBjbGFpbXMgYW5hbHlzdCBtaWdodCB3YW50IHRvIGFuYWx5emUgZnVydGhlci4NCg0KYGBge3J9DQpkYXRfbnVtX25vX21pc3NpbmdzX3NjYWxlZFtvdXRsaWVycyxdDQpwcmVkaWN0KGNjbHVzdF9yZXMsZGF0X251bV9ub19taXNzaW5nc19zY2FsZWRbb3V0bGllcnMsXSkNCmBgYA0KSW4gb3VyIFQtU05FIG1hcCB3ZSBjYW4gaGlnaGxpZ2ggaW4gd2hpY2ggYXJlYXMgb3VyIG91dGxpZXIgYXJlIGx5aW5nLg0KDQpgYGB7cn0NCmRhdGFfdHNuZSRkaXN0YW5jZXMgPC0gZGlzdGFuY2VzDQpnZ3Bsb3QoZGF0YV90c25lLGFlcyh4PVYxLHk9VjIsY29sb3I9ZGlzdGFuY2VzKSkgKyBnZW9tX3BvaW50KCkgKyBzY2FsZV9jb2xvcl9ncmFkaWVudDIoKQ0KYGBgDQoNCk5vdGUgdGhhdCB2YXJpb3VzIG90aGVyIGFuYWx5c2lzIGNhbiBiZSBtYWRlLiBGb3IgZXhhbXBsZSwgdGhlIHNoYXJlIG9mIG91dGxpZXJzIGJ5IGNsdXN0ZXIuDQoNCkFsc28sIHRoZSB0aHJlc2hvbGQgInRvcCA1IiB3YXMgY29tcGxldGVseSBhcmJpdHJhcnkuIElmIHRoZXJlIGFyZSBrbm93biBvdXRsaWVycywgdGhpcyB0aHJlc2hvbGQgY2FuIGJlIGNhbGlicmF0ZWQgaW4gYSB3YXkgc2ltaWxhciB0byBob3cgdGhpcyBpcyBkb25lIGZvciBjbGFzc2lmaWNhdGlvbiBtb2RlbHMgKGUuZy4gY29uc2lkZXJpbmcgZmFsc2UgcG9zaXRpdmVzIC8gbmVnYXRpdmVzIG9uIGEgdmFsaWRhdGlvbiBzZXQpLg0KDQojIyBVc2luZyBEQlNDQU4NCg0KPiBDSEFUOiBIb3cgbWFueSBjYWxjdWxhdGlvbiBhcmUgcmVxdWlyZWQgdG8gb2J0YWluIHRoZSBvdXRsaWVycyBpbiB0aGUgREJTQ0FOIHNlZ21lbnRhdGlvbiA/IChwb3NpdGl2ZSBudW1iZXIpDQoNCkRCU0NBTiBpcyB0aGUgb25seSBhbGdvcml0aG0gYW1vbmcgdGhlIG9uZXMgd2UgYXJlIGZvY3VzaW5nIGhlcmUgdGhhdCBoYXMgYSBidWlsZC1pbiBjb25jZXB0IG9mIG91dGxpZXIgcG9pbnRzLiBIZW5jZSBubyBjYWxjdWxhdGlvbnMgYXJlIHJlcXVpcmVkLg0KDQpgYGB7cn0NCm91dGxpZXJfZGJzY2FuIDwtIHdoaWNoKGhkYnNjYW5fcmVzJGNsdXN0ZXI9PTApDQpvdXRsaWVyX2Ric2Nhbg0KYGBgDQoNCkFsbCA1IG91dGxpZXJzIGZyb20gay1tZWFucyBhcmUgYWxzbyBvdXRsaWVycyBhY2NvcmRpbmcgdG8gdGhlIERCU0NBTiBtZXRob2RvbG9neS4NCg0KYGBge3J9DQpvdXRsaWVycyAlaW4lIG91dGxpZXJfZGJzY2FuDQpgYGANCg0KDQojIyBQcm9iYWJpbHktYmFzZWQgKE1vZGVsLWJhc2VkIGNsdXN0ZXJpbmcpDQoNCkRlZmluaXRpb24gb2Ygb3V0bGllcjogcHJvYmFiaWxpdHkgb2YgYmVsb25naW5nIHRvIHRoZSBhc3NpZ25lZCBjbHVzdGVyIGlzIGJlbG93IDUwJSAodGhpcyBjYW4gaGFwcGVuIHNpbmNlIHdlIGhhdmUgbW9yZSB0aGFuIDIgY2x1c3RlcnMpDQoNCmBgYHtyfQ0Kb3V0bGllcl9nbW0gPC0gd2hpY2gocHJvYmFiX29mX21ham9yaXR5X2NsYXNzPC41KQ0Kb3V0bGllcl9nbW0NCmBgYA0KDQpUaGUgb3V0bGllcnMgaW4gR01NIGRvIG5vdCBpbmNsdWRlIHRoZSA1IG91dGxpZXJzIGZyb20gay1tZWFucw0KDQpgYGB7cn0gDQpvdXRsaWVycyAlaW4lIG91dGxpZXJfZ21tDQpgYGANCg0KQW5kIG9ubHkgdGhlIGZpcnN0IG9uZSBpcyBhbHNvIGFuIG91dGxpZXIgaW4gREJTQ0FOLg0KDQpgYGB7cn0NCm91dGxpZXJfZ21tICVpbiUgb3V0bGllcl9kYnNjYW4NCmBgYA0KDQpBbGwgaW4gYWxsLCB3ZSBzZWUgdGhhdCB0aGVyZSBpcyBubyB1bml2ZXJzYWwgY29uY2VwdCBvZiBhbiBvdXRsaWVyLiBJdCBzdHJvbmdseSBkZXBlbmRzIG9uIHRoZSBjbHN1dGVyaW5nIG1ldGhvZG9sb2d5IGFuZCBob3cgb3V0bGllciBpcyBkZWZpbmVkIGJhc2VkIG9uIHRoYXQuDQoNCklkZWFsbHksIGFuZCB1bnN1cGVydmlzZWQgb3V0bGllciBkZXRlY3Rpb24gYXBwcm9hY2ggaXMgdXNlZCB0byBpZGVudGlmeSBvYnNlcnZhdGlvbnMgZm9yIG1hbnVhbCBpbnNwZWN0aW9uLiBUaGUgcmVzdWx0IG9mIHRoaXMgaW5zcGVjdGlvbiBpcyB0cmFja2VkIHNvIHRoYXQgdGhlcmUgd2lsbCBiZSwgaW4gdGhlIGZ1dHVyZSwgYSBkYXRhIGJhc2UgdG8gdHJhaW4gYSBzdXBlcnZpc2VkIGxlYXJuaW5nIGFsZ29yaXRobS4NCg==